Friday, September 14, 2012

Android OAuth2 REST client with Spring for Android

Overview

In this article, I will try to explain how to design an Android OAuth2 client that can interact with a Smyfony2 back-end implementation using Spring for Android and Spring Social.

Before we start, I need to emphasize a couple of points. First of all, I am not an Android expert; I am still learning. Secondly, I like to leverage existing tools so I have chosen Spring Social for Android for this article. It satisfies my requirements for this tutorial but it may not be suitable for every case. Finally, I will not be explaining the server side implementation in this article. Read my previous FOSRestBundle and FOSOAuthServerBundle articles if you need starters.

Raw Data

Let's first look at the raw data that we will fetch from the server. Because I have been using the Conversation and Message examples for a while now, I will stick with the same pattern in this article. Let's say we would like to retrieve a list of conversations by a specific user from a REST endpoint located at /api/users/4f7f79ac7f8b9a000f000001/conversations.json. As you can see in the JSON output below, the data we will receive is a simple list that contains a single conversation with two messages:

[
    {
        "id": "501ee4b27f8b9aa905000000",
        "created_at": "2012-08-05T14:25:06-0700",
        "modified_at": "2012-08-25T00:08:27-0700",
        "replied_id": "503879eb7f8b9aa505000000",
        "replied_at": "2012-08-25T00:08:27-0700",
        "replied_by": "4f7f79ac7f8b9a000f000001",
        "replied_body": "test",
        "messages": [
            {
                "id": "501ee4b27f8b9aa905000001",
                "user_id": "500b3d027f8b9aeb2f000002",
                "body": "Hello Burak!",
                "created_at": "2012-08-05T14:25:06-0700",
                "modified_at": "2012-08-05T14:25:06-0700"
            },
            {
                "id": "501ef8bb7f8b9aa905000005",
                "user_id": "4f7f79ac7f8b9a000f000001",
                "body": "How are you?",
                "created_at": "2012-08-05T15:50:35-0700",
                "modified_at": "2012-08-05T15:50:35-0700"
            }
        ],
        "from_user": {
            "username": "burcu",
            "name_first": "Burcu",
            "name_last": "Seydioglu",
            "created_at": "2012-07-21T16:36:34-0700",
            "modified_at": "2012-08-05T13:22:06-0700",
            "avatar": "http://s3.amazonaws.com/dev.media.acme.com/avatar/50/00/03/02/07/08/09/00/02/00/00/02/500b3d027f8b9aeb2f000002",
            "id": "500b3d027f8b9aeb2f000002"
        },
        "to_user": {
            "username": "buraks78",
            "name_first": "Burak",
            "name_last": "Seydioglu",
            "created_at": "2012-04-06T16:18:04-0700",
            "modified_at": "2012-09-11T21:54:39-0700",
            "avatar": "http://s3.amazonaws.com/dev.media.acme.com/avatar/04/07/79/00/07/08/09/00/00/00/00/01/4f7f79ac7f8b9a000f000001",
            "id": "4f7f79ac7f8b9a000f000001"
        }
    }
]

Code Structure

We will follow a similar package pattern to the spring-social-facebook package and create three packages:

com.acme.social.api
This package will contain our entity classes. In this particular case, we are talking about three entities: Conversation, Message, and User. This same package will also include our template classes containing our API methods.

com.acme.social.api.json
This packge will contain all the necessary classes that handle serialization/de-serialization of our Java entity classes from/to JSON.

com.acme.social.connect
Finally, this package will contain our connection classes that will bind everything together.

Entity Classes

These classes are pretty straight forward so I will just share the code and keep the discussion to a minimum.

User.java

package com.acme.social.api;

import java.util.Date;

import android.os.Parcel;
import android.os.Parcelable;

public class User implements Parcelable {
    
    protected String id;
    protected String username;
    protected String nameFirst;
    protected String nameLast;
    protected String avatar;
    protected Date createdAt;
    protected Date modifiedAt;
    
    public User(String id, String username, String nameFirst, String nameLast, String avatar, Date createdAt, Date modifiedAt) {
        this.id = id;
        this.username = username;
        this.nameFirst = nameFirst;
        this.nameLast = nameLast;
        this.avatar = avatar;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }
    
    public User(String id, String username, String nameFirst, String nameLast, String avatar) {
        this.id = id;
        this.username = username;
        this.nameFirst = nameFirst;
        this.nameLast = nameLast;
        this.avatar = avatar;
    }
    
    public User(Parcel in) {
        readFromParcel(in);
    }

    public String getAvatar() {
        return avatar;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public String getId() {
        return id;
    }

    public Date getModifiedAt() {
        return modifiedAt;
    }
    
    public String getName() {
     return nameFirst + " " + nameLast;
    }

    public String getNameFirst() {
        return nameFirst;
    }

    public String getNameLast() {
        return nameLast;
    }

    public String getUsername() {
        return username;
    } 
    
    @Override
 public int describeContents() {
  return 0;
 }

 @Override
 public void writeToParcel(Parcel out, int flags) {
  out.writeString(id);
  out.writeString(username);
  out.writeString(nameFirst);
  out.writeString(nameLast);
  out.writeString(avatar);
  out.writeSerializable(createdAt);
  out.writeSerializable(modifiedAt);
 }
 
 private void readFromParcel(Parcel in) {
  id = in.readString();
  username = in.readString();
  nameFirst = in.readString();
  nameLast = in.readString();
  avatar = in.readString();
  createdAt = (Date) in.readSerializable();
  modifiedAt = (Date) in.readSerializable();
 }
 
 public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
  
        public User createFromParcel(Parcel in) {
            return new User(in);
        }
 
        public User[] newArray(int size) {
            return new User[size];
        }
        
    };

}

Message.java

package com.acme.social.api;

import java.util.Date;

import android.os.Parcel;
import android.os.Parcelable;

public class Message implements Parcelable {
    
 private String id;
    private String userId;
    private String body;
    private Date createdAt;
    private Date modifiedAt;
    // database related
    protected String userUsername;
 protected String userNameFirst;
 protected String userNameLast;
 protected String userAvatar;
    
    public Message(String id, String userId, String body, Date createdAt, Date modifiedAt) {
     this.id = id;
        this.userId = userId;
        this.body = body;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }
    
    public Message(String id, String userId, String body, Date createdAt, Date modifiedAt, String userUsername, String userNameFirst, String userNameLast, String userAvatar) {
     this.id = id;
     this.userId = userId;
        this.body = body;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
  this.userUsername = userUsername;
  this.userNameFirst = userNameFirst;
  this.userNameLast = userNameLast;
  this.userAvatar = userAvatar;
 }
    
    public Message(Parcel in) {
     readFromParcel(in);
    }
    
    public String getId() {
        return id;
    }
   
    public String getBody() {
        return body;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public Date getModifiedAt() {
        return modifiedAt;
    }

    public String getUserId() {
        return userId;
    }
    
    public String getUserUsername() {
  return userUsername;
 }
    
    public String getUserNameFirst() {
  return userNameFirst;
 }
    
    public String getUserNameLast() {
  return userNameLast;
 }
 
 public String getUserName() {
  return userNameFirst + " " + userNameLast;
 }

 public String getUserAvatar() {
  return userAvatar;
 }
 
 public void setUserId(String userId) {
  this.userId = userId;
 }

    @Override
 public int describeContents() {
  return 0;
 }

 @Override
 public void writeToParcel(Parcel out, int flags) {
  out.writeString(id);
  out.writeString(userId);
  out.writeString(userUsername);
  out.writeString(userNameFirst);
  out.writeString(userNameLast);
  out.writeString(userAvatar);
  out.writeString(body);
  out.writeSerializable(createdAt);
  out.writeSerializable(modifiedAt);
 }
 
 private void readFromParcel(Parcel in) {
  id = in.readString();
  userId = in.readString();
  userUsername = in.readString();
  userNameFirst = in.readString();
  userNameLast = in.readString();
  userAvatar = in.readString();
  body = in.readString();
  createdAt = (Date) in.readSerializable();
  modifiedAt = (Date) in.readSerializable();
 }
 
 public static final Parcelable.Creator<Message> CREATOR = new Parcelable.Creator<Message>() {
  
        public Message createFromParcel(Parcel in) {
            return new Message(in);
        }
 
        public Message[] newArray(int size) {
            return new Message[size];
        }
        
    };
    
}

Conversation.java

package com.acme.social.api;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import android.os.Parcel;
import android.os.Parcelable;

public class Conversation implements Parcelable {
    
    private String id;
    private User fromUser;
    private User toUser;
    private Date createdAt;
    private Date modifiedAt;
    private String repliedId;
    private Date repliedAt;
    private String repliedBy;
    private String repliedBody;
    private String avatar;
    private List<Message> messages;
    
    public Conversation(String id, User fromUser, User toUser, Date createdAt, Date modifiedAt, String repliedId, Date repliedAt, String repliedBy, String repliedBody) {
        this.id = id;
        this.fromUser = fromUser;
        this.toUser = toUser;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
        this.repliedId = repliedId;
        this.repliedAt = repliedAt;
        this.repliedBy = repliedBy;
        this.repliedBody = repliedBody;
    }
    
    public Conversation(Parcel in) {
     readFromParcel(in);
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public User getFromUser() {
        return fromUser;
    }

    public String getId() {
        return id;
    }

    public List<Message> getMessages() {
        return messages;
    }
    
    public Message getLastMessage() {
     if (messages != null && !messages.isEmpty()) {
      return messages.get(messages.size() - 1);
  }
     return null;
    }

    public Date getModifiedAt() {
        return modifiedAt;
    }
    
    public String getRepliedId() {
        return repliedId;
    }

    public Date getRepliedAt() {
        return repliedAt;
    }

    public String getRepliedBy() {
        return repliedBy;
    }
    
    public String getRepliedBody() {
     return repliedBody;
    }

    public User getToUser() {
        return toUser;
    }
    
    
 public String getAvatar() {
  return avatar;
 }

 public void getAvatar(String avatar) {
  this.avatar = avatar;
 }

 @Override
 public int describeContents() {
  return 0;
 }

 @Override
 public void writeToParcel(Parcel out, int flags) {
  out.writeString(id);
  out.writeParcelable(fromUser, flags);
  out.writeParcelable(toUser, flags);
  out.writeSerializable(createdAt);
  out.writeSerializable(modifiedAt);
  out.writeSerializable(repliedAt);
  out.writeList(messages);
 }
 
 private void readFromParcel(Parcel in) {
     id = in.readString();
     fromUser = in.readParcelable(User.class.getClassLoader());
     toUser = in.readParcelable(User.class.getClassLoader());
     createdAt = (Date) in.readSerializable();
     modifiedAt = (Date) in.readSerializable();
     repliedAt = (Date) in.readSerializable();
     messages = new ArrayList<Message>();
     in.readList(messages, Message.class.getClassLoader());
 }
 
 public static final Parcelable.Creator<Conversation> CREATOR = new Parcelable.Creator<Conversation>() {
  
        public Conversation createFromParcel(Parcel in) {
            return new Conversation(in);
        }
 
        public Conversation[] newArray(int size) {
            return new Conversation[size];
        }
        
    };
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Conversation)) {
           return false;
        }
        return this.getId().equals(((Conversation) obj).getId());
    }

}

You may have noticed the Parcelable interface implemented by all these entities. This is required because all entity class instances will be generated and returned by an IntentService as I will explain in my next article. If you don't know what the Parcelable interface is used for, have a look at my previous A Parcelable Tutorial for Android article.

In addition to the above entity classes, we also need to create a Profile class which represents the current authenticated user:

Profile.java

package com.acme.social.api;

import java.util.Date;
import java.util.List;

import android.os.Parcel;
import android.os.Parcelable;

public class Profile implements Parcelable {
    
    protected String id;
    protected String nameFirst;
    protected String nameLast;
    protected String username;
    protected String email;
    protected String avatar;
    protected Date createdAt;
    protected Date modifiedAt;
    
    public Profile(String id, String nameFirst, String nameLast, String username, String email, String avatar, Date createdAt, Date modifiedAt) {
        this.id = id;
        this.nameFirst = nameFirst;
        this.nameLast = nameLast;
        this.username = username;
        this.email = email;
        this.avatar = avatar;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }

    public Profile(Parcel in) {
        readFromParcel(in);
    }
    
    public String getAvatar() {
        return avatar;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public String getId() {
        return id;
    }

    public Date getModifiedAt() {
        return modifiedAt;
    }

    public String getNameFirst() {
        return nameFirst;
    }

    public String getNameLast() {
        return nameLast;
    }

    public String getUsername() {
        return username;
    } 
    
    public String getEmail() {
        return email;
    } 

 @Override
 public int describeContents() {
  return 0;
 }

 @Override
 public void writeToParcel(Parcel out, int flags) {
  out.writeString(id);
  out.writeString(nameFirst);
  out.writeString(nameLast);
  out.writeString(username);
  out.writeString(email);
  out.writeString(avatar);
  out.writeSerializable(createdAt);
  out.writeSerializable(modifiedAt);
 }
 
 private void readFromParcel(Parcel in) {
  id = in.readString();
  nameFirst = in.readString();
  nameLast = in.readString();
  username = in.readString();
  email = in.readString();
  avatar = in.readString();
  createdAt = (Date) in.readSerializable();
  modifiedAt = (Date) in.readSerializable();
 }
 
 public static final Parcelable.Creator<Profile> CREATOR = new Parcelable.Creator<Profile>() {
  
        public Profile createFromParcel(Parcel in) {
            return new Profile(in);
        }
 
        public Profile[] newArray(int size) {
            return new Profile[size];
        }
        
    };
    
    public User getUser() {
     return new User(id, username, nameFirst, nameLast, avatar, createdAt, modifiedAt);
    }

}

JSON Mapping

Now that our entity classes are defined, we can move onto creating our mapping classes inside the com.acme.social.api.json package. It is pretty straight-forward Jackson configuration as illustrated below:

UserMixin.java

package com.acme.social.api.json;

import java.util.Date;

import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.annotate.JsonSerialize;

@JsonIgnoreProperties(ignoreUnknown = true)
public class UserMixin {
    
    @JsonCreator
 UserMixin(
  @JsonProperty("id") String id, 
  @JsonProperty("username") String username, 
        @JsonProperty("name_first") String nameFirst, 
        @JsonProperty("name_last") String nameLast, 
        @JsonProperty("avatar") String avatar,
        @JsonProperty("created_at") Date createdAt,
        @JsonProperty("modified_at") Date modifiedAt
    ) {}
    
    @JsonProperty("created_at")
 @JsonSerialize(using = DateSerializer.class)
 Date createdAt;
       
    @JsonProperty("modified_at")
 @JsonSerialize(using = DateSerializer.class)
 Date modifiedAt;
}

MessageMixin.java

package com.acme.social.api.json;

import java.util.Date;

import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.annotate.JsonSerialize;

@JsonIgnoreProperties(ignoreUnknown = true)
public class MessageMixin {
    
    @JsonCreator
 MessageMixin(
  @JsonProperty("id") String id, 
        @JsonProperty("user_id") String userId, 
        @JsonProperty("body") String body,
        @JsonProperty("created_at") Date createdAt,
        @JsonProperty("modified_at") Date modifiedAt
    ) {}
    
    @JsonProperty("created_at")
 @JsonSerialize(using = DateSerializer.class)
 Date createdAt;
       
    @JsonProperty("modified_at")
 @JsonSerialize(using = DateSerializer.class)
 Date modifiedAt;
   
}

ConversationMixin.java

package com.acme.social.api.json;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.type.TypeReference;

import com.acme.social.api.Message;
import com.acme.social.api.User;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ConversationMixin {
    
    @JsonCreator
 ConversationMixin(
        @JsonProperty("id") String id, 
        @JsonProperty("from_user") User fromUser,
        @JsonProperty("to_user") User toUser,
        @JsonProperty("created_at") Date createdAt,
        @JsonProperty("modified_at") Date modifiedAt,
        @JsonProperty("replied_id") String repliedId,
        @JsonProperty("replied_at") Date repliedAt,
        @JsonProperty("replied_by") String repliedBy,
        @JsonProperty("replied_body") String repliedBody
    ) {}
    
    @JsonProperty("created_at")
 @JsonSerialize(using = DateSerializer.class)
 Date createdAt;
       
    @JsonProperty("modified_at")
 @JsonSerialize(using = DateSerializer.class)
 Date modifiedAt;
    
    @JsonProperty("replied_at")
 @JsonSerialize(using = DateSerializer.class)
 Date repliedAt;
   
    @JsonProperty("messages")
    @JsonDeserialize(using = MessageListDeserializer.class)
    List<Message> messages;
    
    private static class MessageListDeserializer extends JsonDeserializer<List<Message>> {
        
        @SuppressWarnings("unchecked")
        @Override
        public List<Message> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
            
            ObjectMapper mapper = new ObjectMapper();
            mapper.setDeserializationConfig(ctxt.getConfig());
            jp.setCodec(mapper);
            
            if(jp.hasCurrentToken()) {
                JsonNode dataNode = jp.readValueAsTree();
                if(dataNode != null) {
                    return (List<Message>) mapper.readValue(dataNode, new TypeReference<List<Message>>() {});
                }
            }

            return null;
        }
        
    }
}

ProfileMixin.java

package com.acme.social.api.json;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonDeserialize;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.type.TypeReference;

@JsonIgnoreProperties(ignoreUnknown = true)
public class ProfileMixin {
    
    @JsonCreator
    ProfileMixin(
  @JsonProperty("id") String id, 
        @JsonProperty("name_first") String nameFirst, 
        @JsonProperty("name_last") String nameLast, 
        @JsonProperty("username") String username, 
        @JsonProperty("email") String email, 
        @JsonProperty("avatar") String avatar,
        @JsonProperty("created_at") Date createdAt,
        @JsonProperty("modified_at") Date modifiedAt
    ) {}
    
    @JsonProperty("created_at")
 @JsonSerialize(using = DateSerializer.class)
 Date createdAt;
       
    @JsonProperty("modified_at")
 @JsonSerialize(using = DateSerializer.class)
 Date modifiedAt;
}

You may have noticed the DateSerializer.class references. This custom serializer class is necessary because we need to generate an ISO8601 formatted dates when serializing our entities. This is required not only for consistency purposes - the dates we receive are in this format - but also the format needs to be recognizable by PHP's DateTime constructor when serialized JSON is unmarshalled on the server.

DateSerializer.java

package com.acme.social.api.json;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.JsonSerializer;
import org.codehaus.jackson.map.SerializerProvider;

public class DateSerializer extends JsonSerializer<Date> {
 @Override
 public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
  SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); // iso8601
  String formattedDate = formatter.format(value);
  jgen.writeString(formattedDate);
 }
}

Finally, let's couple our entity classes with their corresponding mixin classes. We will utilize a module class to accomplish this as illustrated below:

AcmeModule.java

package com.acme.social.api.json;

import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.module.SimpleModule;

import com.acme.social.api.Conversation;
import com.acme.social.api.Message;
import com.acme.social.api.Profile;
import com.acme.social.api.User;

public class AcmeModule extends SimpleModule {
    
    public AcmeModule() {
  super("AcmeModule", new Version(1, 0, 0, null));
 }
    
    @Override
 public void setupModule(SetupContext context) {    
  context.setMixInAnnotations(User.class, UserMixin.class);
  context.setMixInAnnotations(Profile.class, ProfileMixin.class);
  context.setMixInAnnotations(Conversation.class, ConversationMixin.class);
  context.setMixInAnnotations(Message.class, MessageMixin.class);
    }
}

Template Classes

Template classes contain all our API methods to interact with the remote server. We will create a template class for each of our entities (unless they are embedded).

UserTemplate.java

package com.acme.social.api;

public class UserTemplate extends BaseTemplate {
    
 private final AcmeTemplate api;
    
    public UserTemplate(AcmeTemplate api, boolean isAuthorized) {
        super(isAuthorized);
        this.api = api;
    }

}

ConversationTemplate.java

package com.acme.social.api;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;

public class ConversationTemplate extends BaseTemplate {
    
 private final AcmeTemplate api;
    
    public ConversationTemplate(AcmeTemplate api, boolean isAuthorized) {
        super(isAuthorized);
        this.api = api;
    }
    
    public List<Conversation> getConversations(String userId) {
     return api.fetchCollection("/users/" + userId + "/conversations.json?limit=" + ConversationTemplate.DEFAULT_LIMIT, Conversation.class, null);
 }
    
    public Conversation getConversation(String userId, String conversationId) {
     return api.fetchObject("/users/" + userId + "/conversations/" + conversationId + ".json", Conversation.class, null);
    }
    
    public List<Message> getMessages(String userId, String conversationId) {
     return api.fetchCollection("/users/" + userId + "/conversations/" + conversationId + "/messages.json", Message.class, null);
    }
    
    public Message postMessage(String userId, String conversationId, Message message) {
     return api.postForObject("/users/" + userId + "/conversations/" + conversationId + "/messages.json", message, Message.class);
    }
    
}

ProfileTemplate.java

package com.acme.social.api;

public class ProfileTemplate extends BaseTemplate {
    
 private final AcmeTemplate api;
    
    public ProfileTemplate(AcmeTemplate api, boolean isAuthorized) {
        super(isAuthorized);
        this.api = api;
    }
    
    public Profile getProfile() {
     return api.fetchObject("/profiles/me.json", Profile.class, null);
    }

    
}

Finally, we create a base template class called AcmeTemplate to tie everything together. This class contains a couple of wrapper methods (i.e. postForObject, fetchObject, fetchCollection) - not a complete list but enough to serve the purpose of this article - to handle URL generation. It also contains a method called registerAcmeJsonModule which binds our JSON setup (AcmeModule) to the REST template provided by Spring for Android and ensures that the property naming strategy is correct (lowercase words separated with underscores).

AcmeTemplate.java

package com.acme.social.api;

import java.io.IOException;
import java.util.List;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.PropertyNamingStrategy;
import org.codehaus.jackson.map.type.CollectionType;
import org.codehaus.jackson.map.type.TypeFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.social.UncategorizedApiException;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.support.ClientHttpRequestFactorySelector;
import org.springframework.social.support.URIBuilder;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import com.acme.social.api.json.AcmeModule;

public class AcmeTemplate extends AbstractOAuth2ApiBinding implements Acme {
    
    private static final String ACME_API_URL = "http://10.0.2.2/app_dev.php/api";
 private UserTemplate userOperations;
 private ProfileTemplate profileOperations;
    private ConversationTemplate conversationOperations;
    
    private ObjectMapper objectMapper;
    
    public AcmeTemplate() {
        initialize();  
    }
    
    public AcmeTemplate(String accessToken) {
  super(accessToken);
  initialize();
 }
    
    public void initSubApis() {
        this.userOperations = new UserTemplate(this, isAuthorized());
        this.profileOperations = new ProfileTemplate(this, isAuthorized());
        this.conversationOperations = new ConversationTemplate(this, isAuthorized());
    }
    
    public UserTemplate userOperations() {
        return this.userOperations;
    }
    
    public ProfileTemplate profileOperations() {
        return this.profileOperations;
    }
    
    public ConversationTemplate conversationOperations() {
        return this.conversationOperations;
    }
    
    private void initialize() {
  registerAcmeJsonModule(getRestTemplate());
  getRestTemplate().setErrorHandler(new DefaultResponseErrorHandler());
  // Wrap the request factory with a BufferingClientHttpRequestFactory so that the error handler can do repeat reads on the response.getBody()
  super.setRequestFactory(ClientHttpRequestFactorySelector.bufferRequests(getRestTemplate().getRequestFactory()));
  initSubApis();
 }
  
 private void registerAcmeJsonModule(RestTemplate restTemplate2) {
        
  this.objectMapper = new ObjectMapper();    
  this.objectMapper.registerModule(new AcmeModule());
  this.objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy());
        
  List<HttpMessageConverter<?>> converters = getRestTemplate().getMessageConverters();
  for (HttpMessageConverter<?> converter : converters) {
   if(converter instanceof MappingJacksonHttpMessageConverter) {
    MappingJacksonHttpMessageConverter jsonConverter = (MappingJacksonHttpMessageConverter) converter;
    jsonConverter.setObjectMapper(objectMapper);
   }
  }
 }
 
 public <T> T postForObject(String path, Object request, Class<T> responseType) {
  URIBuilder uriBuilder = URIBuilder.fromUri(ACME_API_URL + path);
  return getRestTemplate().postForObject(uriBuilder.build(), request, responseType);
 }
 
 public <T> T fetchObject(String path, Class<T> type, MultiValueMap<String, String> queryParameters) {
  URIBuilder uriBuilder = URIBuilder.fromUri(ACME_API_URL + path);
  if (queryParameters != null) {
   uriBuilder.queryParams(queryParameters);
  }
  return getRestTemplate().getForObject(uriBuilder.build(), type);
 }
 
 public <T> List<T> fetchCollection(String path, Class<T> type, MultiValueMap<String, String> queryParameters) {
  URIBuilder uriBuilder = URIBuilder.fromUri(ACME_API_URL + path);
  if (queryParameters != null) {
   uriBuilder.queryParams(queryParameters);
  }
  JsonNode dataNode = getRestTemplate().getForObject(uriBuilder.build(), JsonNode.class);
  return deserializeDataList(dataNode, type);
 }
 
 @SuppressWarnings("unchecked")
 private <T> List<T> deserializeDataList(JsonNode jsonNode, final Class<T> elementType) {
  try {
   CollectionType listType = TypeFactory.defaultInstance().constructCollectionType(List.class, elementType);
   return (List<T>) objectMapper.readValue(jsonNode, listType);
  } catch (IOException e) {
   throw new UncategorizedApiException("Error deserializing data from: " + e.getMessage(), e);
  }
 }

}

Connection Classes

Before we go into code deatils, have a look at the below UML class diagram that explains our setup. We are basically going to model our design after existing packages already provided by Spring Social; see the Facebook package by Spring Social for reference.

Spring Social - UML Diagram

In the above UML class diagram, light green boxes represent our custom classes while the yellow ones represent classes provided by Spring Social.

AcmeConnectionFactory
This class extends the OAuth2ConnectionFactory class. As you can already infer from the naming convention, this class is responsible for generating an OAuth2Connection. Its constructor injects our own custom AcmeServiceProvider and AcmeAdapter class instances into the base connection factory class.

AcmeConnectionFactory.java

package com.acme.social.connect;

import com.acme.social.api.Acme;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;

public class AcmeConnectionFactory extends OAuth2ConnectionFactory<Acme> {

 public AcmeConnectionFactory(String clientId, String clientSecret, String authorizeUrl, String tokenUrl) {
  super("Acme", new AcmeServiceProvider(clientId, clientSecret, authorizeUrl, tokenUrl), new AcmeAdapter());
 }

}

AcmeAdapter
As can be seen in the UML diagram above, AcmeAdapter is a component of OAuth2ConnectionFactory and OAuth2Connection classes. This allows us to inject our authenticated user information (i.e. id, username, and avatar) to the active connection so that we can extract authenticated user details from the connection later when needed in the application. In fact, if you examine the SQLite database table containing the connection information after a successful authentication, you can see that there is a column for provider user id and this class serves as the key mechanism to populate that column.

AcmeAdapter.java

package com.acme.social.connect;

import com.acme.social.api.Acme;
import com.acme.social.api.Profile;

import org.springframework.social.ApiException;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfileBuilder;

public class AcmeAdapter implements ApiAdapter<Acme> {
    
    @Override
    public boolean test(Acme acme) {
  try {
   return true;
  } catch (ApiException e) {
            e = null;
   return false;
  }
 }

    @Override
 public void setConnectionValues(Acme acme, ConnectionValues values) {
  Profile profile = acme.profileOperations().getProfile();
  values.setProviderUserId(profile.getId());
  values.setDisplayName(profile.getUsername());
  values.setImageUrl(profile.getAvatar());
 }

    @Override
 public org.springframework.social.connect.UserProfile fetchUserProfile(Acme acme) {
     
  Profile profile = acme.profileOperations().getProfile();
        
  return new UserProfileBuilder().setName(profile.getUsername())
            .setFirstName(profile.getNameFirst())
            .setLastName(profile.getNameLast())
            .setEmail(profile.getEmail())
            .setUsername(profile.getUsername())
            .build();
        
 }
 
    @Override
 public void updateStatus(Acme acme, String message) {
  acme.profileOperations().updateStatus(message);
 }

}

AcmeServiceProvider
This class extends AbstractOAuth2ServiceProvider and is responsible for binding our AcmeTemplate class with the OAuth2Template class that makes OAuth2 calls using the provided REST template.

AcmeServiceProvider.java

package com.acme.social.connect;

import com.acme.social.api.acme;
import com.acme.social.api.AcmeTemplate;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
import org.springframework.social.oauth2.OAuth2Template;

public class AcmeServiceProvider extends AbstractOAuth2ServiceProvider<Acme> {

 public AcmeServiceProvider(String clientId, String clientSecret, String authorizeUrl, String tokenUrl) {
  super(new OAuth2Template(clientId, clientSecret, authorizeUrl, tokenUrl));
 }
    
    @Override
 public Acme getApi(String accessToken) {
  return new AcmeTemplate(accessToken);
 }
 
}

And with this last class defined, our setup is complete. In my next article, I will explain how to use these classes inside an Android application.

4 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. I can't find "Acme.java" and "BaseTemplate.java", you can adds please ?

    ReplyDelete