Consume Twitter REST API v1.1 with ADF Mobile

The new version of Twitter REST API has been out for a while now, and one of the biggest changes that needs to be implemented now in order to consume it, is that for GET methods such as search / tweets, all the requests need to be sent with OAuth signature as part of the Authorisation header, so I’ll show a little work around on how to perform these requests by creating an ADF Mobile application that calls the GET search/tweets method with a OAuth 1.0a HMAC-SHA1 signature as part of the Authorisation header…

Before starting make sure that you:

  • Have your own Twitter account (pretty obvious I know).
  • Have created your Twitter app from https://apps.twitter.com
  • Already generated the API Keys for your Twitter app.

Hands On!

    1. Generate a new ADF Mobile Application.
    2. Create a new URL Connection in the Application Resources section, the correct URL endpoint should be https://api.twitter.com.  We will call this connection from our java class later:
      Screen Shot 2014-04-11 at 6.52.41 PM
      Don´t worry about the not accessible message, that´s why we are generating our OAuth signature!
    3. Considering the JSON expected results explained in the documentation, on the ViewController project create a new Java Class to handle the response:
      import oracle.adfmf.json.JSONArray;
      
      /**
       * This class is used to hanlde the response from Twitter's search method.
       * @version 1.0
       */
      public class TwitterResponse {
      
          /** JSON Array to handle error retrieved by search method. **/
          private JSONArray errors;
      
          /** JSON Array to handle the response. **/
          private JSONArray statuses;
      
          //Getters and Setters
      }
      

      Notice that the variables names in this Java Class must match the exact JSON response objects in order to be parsed correctly.

    4. Now we need to create another Java Class to be used in the .amx Page to show the information retrieved from the method:
      /**
       * This class is used to show a Twitter search record.
       * @version 1.0
       */
      public class TwitterSearchRecord {
      
          /** The name of the user who created the tweet. **/
          private String userName;
      
          /** The Id of the user who created tweet. **/
          private String userId;
      
          /** The tweet message. **/
          private String message;
      
          /** The user's profile photo URL. **/
          private String profilePhoto;
      
          // Getters and Setters
      }
      
    5. Create a new feature in the adfmf-feature.xml file and specify the content as AMX Page.
      Screen Shot 2014-04-11 at 9.07.34 PM
    6. In order to perform a successful call we need to create our OAuth signature as described here. A couple of important things need to be considered:
      1. We need to generate the oauth_nonce:  a Base 64 encoding unique token of random data which needs to be created on every request.
      2. We need to create the signature using HMAC-SHA1 method.

      To accomplish this, I created a JavaScript resource which exposes 2 functions, one to retrieve the nonce, and another one to retrieve the signature and save it as functions.js:

      // function to get the oauth_nonce
      getOauthNonce = function(){
         var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
         var result = "";
         for (var i = 0; i < 6; ++i) {
                 var rnum = Math.floor(Math.random() * chars.length);
                  result += chars.substring(rnum, rnum+1);
          }
          return result;
      }
      
      //Function to generate the signature
      getSignature = function(){
          try{
               var args = arguments;
               //Call the Crypto API and create the HMAC String using
               //the signature baseString and the both the consumer and application secrets
               var hmacString = Crypto.HMAC(Crypto.SHA1, args[0], args[1], { asString:true });
               //encode the String to base64
               var signature = encodeBase64(hmacString);
               return signature;
          }catch(e){
              alert(e);
          }
      };
      

      As you can see, to generate the HMAC SHA1 String we use Crypto library from Google (I suggest to create a separate JS resource to include it as part of the feature content),  you can also refer to the getOauthNonce method from here.  For the encodeBase64 method I’m using a nice conversion function to ensure cross browser compatibility.

    7. Don’t forget to Include the reference to these JavaScript files in the  adfmf-feature.xml file.

Putting all Together!

  1. Create a Java class and include the following methods to perform the call (this Java class is going to be exposed as Data Control later) ,
    public class TwitterSearchOperations {
        /** This variable holds the rest request value **/
        protected String jsonResponse;
    
        /** The records to be retrieved in the amx page **/
        protected static TwitterSearchRecord[] searchRecords;
    
        /** used for the encodeURIComponent function */
        private static final BitSet dontNeedEncoding;
    
        /**
         * Constructor of the class.
         */
        public TwitterSearchOperations() {
            super();
        }
    
        /**
         * This method return a list of tweets based on a search text from the user
         * @param searchText the criteria for searchinf tweets
         * @return a list of tweets meeting the search criteria
         */
        public TwitterSearchRecord[] searchTweets(String searchText) {
            //Validates the searchText
            if(searchText == null || searchText.equals("")){
                return null;
            }
    
            try {
                //Create a new instance of the Service Adapter
                RestServiceAdapter restServiceAdapter = Model.createRestServiceAdapter();
                // Clear any previously request
                restServiceAdapter.clearRequestProperties();
                // Set the connection to search in connections.xml file
                restServiceAdapter.setConnectionName("TwitterConnection");
                //Encode the search String to UTF-8
                String value = encodeURIComponent(searchText);
                // Set the Request type
                restServiceAdapter.setRequestType(RestServiceAdapter.REQUEST_TYPE_GET);
                //Get the header value for Authorization
                String authorizationString = getAuthorizationString(value);
                //Specify the content type
                restServiceAdapter.addRequestProperty("Content-Type", "application/json");
                restServiceAdapter.addRequestProperty("X-HostCommonName", "api.twitter.com");
                restServiceAdapter.addRequestProperty("Host", "api.twitter.com");
                restServiceAdapter.addRequestProperty("X-Target-URI", "https://api.twitter.com");
                restServiceAdapter.addRequestProperty("Connection", "Keep-Alive");
                restServiceAdapter.addRequestProperty("Authorization", authorizationString);
                // Specify the number of retries
                restServiceAdapter.setRetryLimit(0);
    
                // Set the URI which is defined after the endpoint in the connections.xml.
                // The request is the endpoint + the URI being set
                restServiceAdapter.setRequestURI("/1.1/search/tweets.json?q=" + value);
                this.setJsonResponse(restServiceAdapter.send(""));
                JSONBeanSerializationHelper helper = new JSONBeanSerializationHelper();
                //Parse the JSON Response
                TwitterResponse twitterBean =
                    (TwitterResponse)helper.fromJSON(TwitterResponse.class, this.getJsonResponse());
                //Validate the results
                if (twitterBean != null && twitterBean.getStatuses() != null) {
                    this.searchRecords = new TwitterSearchRecord[twitterBean.getStatuses().length()];
                    for (int i = 0; i < twitterBean.getStatuses().length(); i++) {
                        TwitterSearchRecord record = new TwitterSearchRecord();
                        JSONObject properties = (JSONObject)twitterBean.getStatuses().get(i);
                        //Map the JSON Response records to Search records object
                        fillTwitterRecord(properties, record);
                        this.searchRecords[i] = record;
                    }
                }
            } catch (Exception e) {
                throw new AdfException(e);
            }
            return this.searchRecords;
        }
    
        //private methods
    
        /**
         * This method constructs the authorization String to be added as the Authorization property value.
         * @param searchText the query search performed by the user
         * @return authorizationString a String value containing the authorization key
         */
        private String getAuthorizationString(String searchText) {
            String authorizationString = null;
            try {
                //Generate the oauthTimeStamp
                String oauthTimestamp = String.valueOf(System.currentTimeMillis() / 1000);
                //Retrieve the oauth_nonce from JS method
                String oauthNonce =
                    (String)AdfmfContainerUtilities.invokeContainerJavaScriptFunction("SearchFeature", "getOauthNonce",
                                                                                      new Object[] { });
                //Encode the application Secret
                String applicationSecret = URLEncoder.encode("yourAccessTokenSecret)", "UTF-8");
                //Encode the consumer secret
                String consumerSecret = URLEncoder.encode("yourConsumerSecret", "UTF-8");
                String consumerKey = "youConsumerKey";
                String oauthToken = "yourOauthToken(Access Token)";
                //Create the signatureBase String
                String signatureBaseString =
                    getSignatureBaseString(consumerKey, oauthNonce, oauthTimestamp, oauthToken, searchText);
                //Get the signature from JS method
                String signature =
                    (String)AdfmfContainerUtilities.invokeContainerJavaScriptFunction("SearchFeature", "getSignature",
                                                                                      new Object[] { signatureBaseString,
                                                                                                     consumerSecret + "&" +
                                                                                                     applicationSecret });
                //Construct the header value
                authorizationString =
                        "OAuth oauth_consumer_key=\"" + consumerKey + "\"," + "oauth_nonce=\"" + oauthNonce + "\", " +
                        "oauth_signature=\"" + URLEncoder.encode(signature, "UTF-8") + "\"," +
                        "oauth_signature_method=\"HMAC-SHA1\", " + "oauth_timestamp=\"" + oauthTimestamp + "\", " +
                        "oauth_version=\"1.0\"," + "oauth_token=\"" + URLEncoder.encode(oauthToken, "UTF-8") + "\"";
    
            } catch (UnsupportedEncodingException e) {
                throw new AdfException(e);
            }
            return authorizationString;
        }
    
        /**
         * This method retrieves the correct signatureBase String.
         * @param consumerKey
         * @param oauthNonce
         * @param oauthTimestamp
         * @param oauthToken
         * @param searchValue the query search performed by the user
         * @return signatureBaseString for signature
         */
        private String getSignatureBaseString(String consumerKey, String oauthNonce, String oauthTimestamp,
                                              String oauthToken, String searchValue) {
            String signatureBaseString = null;
            try {
                signatureBaseString =
                        "GET" + "&" + URLEncoder.encode("https://api.twitter.com/1.1/search/tweets.json", "UTF-8") + "&" +
                        URLEncoder.encode("oauth_consumer_key=" + consumerKey, "UTF-8") +
                        URLEncoder.encode("&" + "oauth_nonce=" + oauthNonce, "UTF-8") +
                        URLEncoder.encode("&" + "oauth_signature_method=" + "HMAC-SHA1", "UTF-8") +
                        URLEncoder.encode("&" + "oauth_timestamp=" + oauthTimestamp, "UTF-8") +
                        URLEncoder.encode("&" + "oauth_token=" + oauthToken, "UTF-8") +
                        URLEncoder.encode("&" + "oauth_version=" + "1.0", "UTF-8") +
                        URLEncoder.encode("&q=" + searchValue, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new AdfException(e);
            }
            return signatureBaseString;
    
        }
    
        /**
         * This method maps the properties of the JSON response to a record to show in the application.
         * @param properties JSON object with the properties needed to display
         * @param record the object on which all the properties are going to be filled
         */
        private void fillTwitterRecord(JSONObject properties, TwitterSearchRecord record) {
            try {
                record.setMessage(properties.getString("text"));
                JSONObject user = properties.getJSONObject("user");
                record.setProfilePhoto(user.getString("profile_image_url"));
                record.setUserName(user.getString("name"));
                record.setUserId(user.getString("screen_name"));
            } catch (JSONException e) {
                throw new AdfException(e);
            }
        }
    
        /**
         * Escapes all characters except the following: alphabetic, decimal digits, - _ . ! ~ * ' ( )
         * @param input A component of a URI
         * @return the escaped URI component
         */
        private static String encodeURIComponent(String input) {
            if (input == null) {
                return input;
            }
            StringBuffer filtered = new StringBuffer(input.length());
            char c;
            for (int i = 0; i < input.length(); ++i) {
                c = input.charAt(i);
                if (dontNeedEncoding.get(c)) {
                    filtered.append(c);
                } else {
                    final byte[] b = charToBytesUTF(c);
                    for (int j = 0; j < b.length; ++j) {
                        filtered.append('%');
                        filtered.append("0123456789ABCDEF".charAt(b[j] >> 4 & 0xF));
                        filtered.append("0123456789ABCDEF".charAt(b[j] & 0xF));
                    }
                }
            }
            return filtered.toString();
        }
        //Static content
        static {
            dontNeedEncoding = new BitSet(256);
    
            // a-z
            for (int i = 97; i <= 122; ++i) {
                dontNeedEncoding.set(i);
            }
            // A-Z
            for (int i = 65; i <= 90; ++i) {
                dontNeedEncoding.set(i);
            }
            // 0-9
            for (int i = 48; i <= 57; ++i) {
                dontNeedEncoding.set(i);
            }
    
            // '()*
            for (int i = 39; i <= 42; ++i) {
                dontNeedEncoding.set(i);
            }
            dontNeedEncoding.set(33); // !
            dontNeedEncoding.set(45); // -
            dontNeedEncoding.set(46); // .
            dontNeedEncoding.set(95); // _
            dontNeedEncoding.set(126); // ~
        }
    
        //Getters and Setters
    
    
  2. Expose this class as DataControl (right click on the Java file from the Application Navigator and select “Create Data Control”).
  3. Now just drag and drop the created method as a Parameter Form and the Result as List View into your amx page:Screen Shot 2014-04-12 at 8.38.59 PM
  4. Deploy your application to your simulator and start searching for tweets!:
    Screen Shot 2014-04-12 at 8.46.18 PM

You can download the sample project to see with more detail the solution using Crypto.js and the source Java Code. Remember that for this use case, you’re using your consumer Key and application Secret so every search performed to Twitter will be on your behalf.

Please comment if this tip was helpful and enjoy!

 

Advertisements

Tags: , , ,

About Christian Silva

I'm an IT specialist and friend who likes to keep learning about new technologies. As an Oracle ADF Implementation Specialist, I've decided to create this blog to share my knowledge in ADF Mobile, ADF and any other topic I believe I can contribute with.

3 responses to “Consume Twitter REST API v1.1 with ADF Mobile”

  1. spandy says :

    Christian I have downloaded the sample project & I have deployed it in my android device & after putting a search text & tapping on search tweets button I was getting the error that HTTP Status Code 401 Unauthorized: The request requires user authentication …. Can you please help me to fix this issue… seeking for help

    • Christian Silva says :

      Hi spandy. Did you set your access token secret and consumer secret in the Java class exposed as Data control? It looks like this values are not correctly set, please confirm.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: