Before we jump into the steps, i assume the reader has a basic working knowledge about Rave.

To add a person as your friend, an association must exist between you and the person whom you want to add as your friend.

To maintain this association we use the PERSON_ASSOCIATION table. The association table contains 3 columns viz. follower, followedBy and status.

Let A and B be 2 users and A wants to add B as his friend. To establish a relationship between these 2 users, user A must send a friend request to user B.

Sending Friend Requests:

This would be stored in the DB as follows.

Follower – A

Followedby – B

Status – Sent.

Follower – B

Followedby – A

Status – Received.

A snapshot of the data in the DB is shown below

friendRequestDB.png

Where

FOLLOWER_ID – 1 is the entity ID of USER A

FOLLOWEDBY_ID – 2 represents USER B who should be displayed in the friends list of USER A.

STATUS – Specifies the status of the relationship(whether a friend request is sent/ received /accepted)

One important thing to note here is that, adding a friend is mutual. i.e If A adds B as his friend and incase B accepts the request, then B should be in the friend list of A and A should be in friend list of B. That is why we have 2 entries in the DB.

In order to make this work, we would have to add a method in JpaPersonRepository.java

   1 public boolean addFriend(String friendUsername, String username) {
   2         JpaPersonAssociation senderItem = new JpaPersonAssociation();           senderItem.setFollower(personConverter.convert(findByUsername(username)));
   3         senderItem.setFollowedby(personConverter.convert(findByUsername(friendUsername)));
   4         senderItem.setStatus(FriendRequestStatus.SENT);
   5         senderItem = saveOrUpdate(senderItem.getEntityId(), manager, senderItem);
   6 
   7         JpaPersonAssociation receiverItem = new JpaPersonAssociation();
   8         receiverItem.setFollower(personConverter.convert(findByUsername(friendUsername)));
   9         receiverItem.setFollowedby(personConverter.convert(findByUsername(username)));
  10         receiverItem.setStatus(FriendRequestStatus.RECEIVED);
  11         receiverItem = saveOrUpdate(receiverItem.getEntityId(), manager, receiverItem);
  12 
  13         if(senderItem.getEntityId()!=null && receiverItem.getEntityId()!=null)
  14                 return true;
  15         else
  16                 return false;
  17 }

The repository would be implementing an interface, so add a method declaration in PersonRepository.java. The same would be the case for any classes which implements any interface.

The repository would be called by a service, which in our case is the DefaultUserService.java

   1    public boolean addFriend(String friendUsername, String username) {
   2         return personRepository.addFriend(friendUsername,username);
   3    }

The service can be called by many controllers and APIs. In our case, the service would be called by PersonApi. The reason is, URL hits would be mapped by controllers but RPC and REST calls would be mapped by APIs and adding a friend is done by an RPC call from javascript.

The PersonApi should have a method like this for adding a friend.

   1    @ResponseBody
   2    @RequestMapping(value = "{friendUsername}/addfriend", method = RequestMethod.POST)
   3    public RpcResult<Boolean> addFriend(@PathVariable final String friendUsername) {
   4         return new RpcOperation<Boolean>() {
   5                 @Override
   6                 public Boolean execute() {
   7                         try {
   8                         String name = URLDecoder.decode(friendUsername, "UTF-8");
   9                         boolean result = userService.addFriend(name, userService.getAuthenticatedUser().getUsername());
  10                         return result;
  11                         } catch (UnsupportedEncodingException e) {
  12                                 return false;
  13                         }
  14                 }
  15         }.getResult();
  16    }

Now let’s come to the UI part of it. Assume that the list of users in Rave is displayed to the user A and a link is available nearby each user to add him as a friend. So now to add user B as a friend, user A would click on ‘Add’ option near user B.

addFriend.png

This would call the addFriend method in rave_person_profile.js which would in turn call a method in rave_api.js which would trigger an RPC call captured by the PersonApi.java

function addFriend(userId, username){
        $('#friendStatusButtonHolder'+userId).hide();
        rave.api.rpc.addFriend({friendUsername : username,
            successCallback: function(result) {
                rave.personprofile.addFriendRequestUI(username);
                $('#friendStatusButtonHolder'+userId).empty();
                $('#friendStatusButtonHolder'+userId)
                    .append(
                            $("<a/>")
                            .attr("href", "#")
                            .attr("id", userId)
                            .attr("onclick", "rave.personprofile.removeFriendRequestSent(" +
                               userId+", '" + username+"');")
                            .text(rave.getClientMessage("common.cancel.request"))
                    );
                $('#friendStatusButtonHolder'+userId).show();
            }
        });
}

The rave_api.js would contain this method.

   function addFriend(args) {
          var user = encodeURIComponent(encodeURIComponent(args.friendUsername));
          $.post(rave.getContext() + path + "person/" + user + "/addfriend",
          function(result) {
              if (result.error) {
                  handleRpcError(result);
              }
              else {
                  if (typeof args.successCallback == 'function') {
                       args.successCallback(result);
                  }
              }
         }).error(handleError);
   }

Receiving Friend Request:

Now that user A has sent his friend Request to user B, user B should be made aware of the friend request from A.

To display the lists in the profile page, first we have to set the data into the model so that it can be displayed in the JSP. To set the data in the model, we should make the following changes in the ProfileController.java

   1 final NavigationMenu topMenu = new NavigationMenu("topnav");
   2 
   3 NavigationItem friendRequestItems = new NavigationItem("page.profile.friend.requests", String.valueOf(friendRequests.size()) , "#");
   4 for(Person request : userService.getFriendRequestsReceived(username)) {
   5         NavigationItem childItem = new NavigationItem((request.getDisplayName()!=null && !request.getDisplayName().isEmpty())? request.getDisplayName() : request.getUsername(), request.getUsername(), "#");
   6         friendRequestItems.addChildNavigationItem(childItem);
   7 }
   8 topMenu.addNavigationItem(friendRequestItems);
   9 topMenu.getNavigationItems().addAll((ControllerUtils.getTopMenu(view, refPageId, user, false).getNavigationItems()));
  10 model.addAttribute(topMenu.getName(), topMenu);

The userService should now add a method to get the list of friend requests received from the DB by calling the repository.

The below method should be added to JpaPersonRepository.java

   1 public List<Person> findFriendRequestsReceived(String username) {
   2    TypedQuery<JpaPerson> friends = manager.createNamedQuery(JpaPerson.FIND_FRIENDS_BY_USERNAME, JpaPerson.class);
   3    friends.setParameter(JpaPerson.USERNAME_PARAM, username);
   4    friends.setParameter(JpaPerson.STATUS_PARAM, FriendRequestStatus.RECEIVED);
   5    return CollectionUtils.<Person>toBaseTypedList(friends.getResultList());
   6 }

The query is “select a.followedby from JpaPersonAssociation a where a.follower.username = :username and a.status = :status”

So once the list of friend requests is set to the model, we can display it in the jsp as we want. Below implementation displays it as a dropdown box with accepting and declining options. This is added to the navbar.tag so that it gets displayed in the top navigation menu.

<c:when test="${navItem.name=='page.profile.friend.requests'}">
  <li class="dropdown"><a href="#" data-toggle="dropdown" class="dropdown-toggle friendRequestDropdownLink"><fmt:message key="${navItem.name}"/>(${navItem.nameParam})</a>
  <c:choose>
  <c:when test="${navItem.hasChildren}">
        <ul class="dropdown-menu friendRequestDropdown">
<c:forEach items="${navItem.childNavigationItems}" var="childItem">
                <li class="requestItem">${childItem.name}
                        <a class="acceptFriendRequest" id="${childItem.nameParam}" href="#"><i class="icon-ok"></i></a>
                        <a class="declineFriendRequest" id="${childItem.nameParam}" href="#"><i class="icon-remove"></i></a>
                </li>
        </c:forEach>
        </ul>
  </c:when>
  <c:otherwise>
<ul class="dropdown-menu">
                <li>No Friend Requests</li>
        </ul>
  </c:otherwise>
  </c:choose>
  </li>
</c:when>

The dropdown for friend request would look like this.

friendRequest.png

The user can either accept the friend request or decline the friend request.

Accepting Friend Requests:

When user B accepts the friend request sent by user A, then we should do another RPC call to persist the relationship.

The accept friend request method in rave_person_profile.js looks like this

function acceptFriendRequest(userId, username){
   $('#friendStatusButtonHolder'+userId).hide();
   rave.api.rpc.acceptFriendRequest({friendUsername : username,
       successCallback: function(result) {
           rave.personprofile.removeFriendRequestReceivedUI(username);
           $('#friendStatusButtonHolder'+userId).empty();
           $('#friendStatusButtonHolder'+userId)
               .append(
                       $("<a/>")
                       .attr("href", "#")
                       .attr("id", userId)
                       .attr("onclick", "rave.personprofile.removeFriend(" +
                           userId+", '" + username+"');")
                       .text(rave.getClientMessage("common.remove"))
               );
           $('#friendStatusButtonHolder'+userId).show();
       }
    });
}

The RPC function call in rave_api.js is

   function acceptFriendRequest(args) {
   var user =  encodeURIComponent(encodeURIComponent(args.friendUsername));
   $.post(rave.getContext() + path + "person/" + user + "/acceptfriendrequest",
        function(result) {
             if (result.error) {
                    handleRpcError(result);
             }
             else {
                    if (typeof args.successCallback == 'function') {
                        args.successCallback(result);
                    }
             }
        }).error(handleError);
   }

You may note that we have encoded the username before we perform the RPC calls. The reason for encoding the username is Rave supports OpenId login and OpenId usernames contains special characters and URL like usernames which when passed without encoding might cause problems. So we encode it and decode it at the receiving end in the PersonApi.java

The PersonApi.java method which captures the RPC call is

   1 @ResponseBody
   2 @RequestMapping(value = "{friendUsername}/acceptfriendrequest", method = RequestMethod.POST)
   3 public RpcResult<Boolean> acceptFriendRequest(@PathVariable final String friendUsername) {
   4     return new RpcOperation<Boolean>() {
   5         @Override
   6         public Boolean execute() {
   7                 try{
   8                         boolean result = userService.acceptFriendRequest(URLDecoder.decode(friendUsername, "UTF-8"), userService.getAuthenticatedUser().getUsername());
   9                         return result;
  10                 }catch (UnsupportedEncodingException e) {
  11                 return false;
  12 }
  13 }
  14     }.getResult();
  15 }

The Repository method would retrieve the old values, change the status to accepted and then store it back to the DB.

   1 public boolean acceptFriendRequest(String friendUsername, String username) {
   2         TypedQuery<JpaPersonAssociation> query = manager.createNamedQuery(JpaPersonAssociation.FIND_ASSOCIATION_ITEM_BY_USERNAMES, JpaPersonAssociation.class);
   3        query.setParameter(JpaPersonAssociation.FOLLOWER_USERNAME, username);
   4        query.setParameter(JpaPersonAssociation.FOLLOWEDBY_USERNAME, friendUsername);
   5        JpaPersonAssociation receiverItem = getSingleResult(query.getResultList());
   6        receiverItem.setStatus(FriendRequestStatus.ACCEPTED);
   7        receiverItem = saveOrUpdate(receiverItem.getEntityId(), manager, receiverItem);
   8 
   9         query = manager.createNamedQuery(JpaPersonAssociation.FIND_ASSOCIATION_ITEM_BY_USERNAMES, JpaPersonAssociation.class);
  10        query.setParameter(JpaPersonAssociation.FOLLOWER_USERNAME, friendUsername);
  11        query.setParameter(JpaPersonAssociation.FOLLOWEDBY_USERNAME, username);
  12        JpaPersonAssociation senderItem = getSingleResult(query.getResultList());
  13        senderItem.setStatus(FriendRequestStatus.ACCEPTED);
  14        senderItem = saveOrUpdate(senderItem.getEntityId(), manager, senderItem);
  15 
  16         if(receiverItem.getEntityId()!=null && senderItem.getEntityId()!=null)
  17                 return true;
  18         else
  19                 return false;
  20 }

DB snapshot after friend request is accepted.

friendDB.png

Removing Friends/Friend Requests:

For Removing friends or friend request, the RPC calls are similar to Adding friends but the repository would search for records from the DB and remove them instead of inserting new ones. For further more detailed implementation, download the Rave source code and start Raving :)

StepsAddFriends (last edited 2012-08-24 19:43:34 by viknesb)