org.instagram4j.DefaultInstagramClient.java Source code

Java tutorial

Introduction

Here is the source code for org.instagram4j.DefaultInstagramClient.java

Source

/**
 * Copyright (C) 2014 James Jory (james.b.jory@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.instagram4j;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.URISyntaxException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.EncoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.net.URLCodec;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.AllClientPNames;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.instagram4j.entity.Comment;
import org.instagram4j.entity.Location;
import org.instagram4j.entity.Media;
import org.instagram4j.entity.Relationship;
import org.instagram4j.entity.RelationshipAction;
import org.instagram4j.entity.Subscription;
import org.instagram4j.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.MappingJsonFactory;

/**
 * Default driver implementation that accesses the API using the Apache Commons 
 * HTTP client library and parses responses using the Jackson JSON library. 
 */
public class DefaultInstagramClient implements InstagramClient {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultInstagramClient.class);

    private static final JsonFactory JSON_FACTORY = new MappingJsonFactory();
    private static final URLCodec ENCODER = new URLCodec("UTF-8");

    private static final String HMAC_SHA256_ALGO = "HmacSHA256";

    private final String clientId;
    private final String clientSecret;
    private String accessToken;

    private boolean signedHeaderEnabled;
    private String clientIps;

    private long autoThrottle;
    private long lastCallTicks;

    public DefaultInstagramClient(String clientId, String clientSecret) {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
    }

    public DefaultInstagramClient(String clientId, String clientSecret, String accessToken) {
        this(clientId, clientSecret);
        this.accessToken = accessToken;
    }

    @Override
    public String getClientId() {
        return clientId;
    }

    @Override
    public String getClientSecret() {
        return clientSecret;
    }

    @Override
    public boolean isSignedHeaderEnabled() {
        return signedHeaderEnabled;
    }

    @Override
    public void setSignedHeaderEnabled(boolean enabled) {
        signedHeaderEnabled = enabled;
    }

    public String getClientIps() {
        return clientIps;
    }

    public void setClientIps(String clientIps) {
        this.clientIps = clientIps;
    }

    @Override
    public String getAccessToken() {
        return accessToken;
    }

    @Override
    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    @Override
    public long getAutoThrottle() {
        return autoThrottle;
    }

    @Override
    public void setAutoThrottle(long minDelay) {
        this.autoThrottle = minDelay;
    }

    @Override
    public Result<User> getCurrentUser() throws InstagramException {
        return getUser("self");
    }

    @Override
    public Result<User> getUser(String userId) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/users/" + urlEncode(userId));
        return queryEntity(uri.toString(), User.class);
    }

    @Override
    public Result<User[]> getFollowers(String userId, int count) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/users/%s/followed-by", urlEncode(userId)),
                Parameter.as("count", count));
        return queryEntities(uri.toString(), User.class);
    }

    @Override
    public Result<User[]> searchUsers(String query, int count) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/users/search", Parameter.as("q", query),
                Parameter.as("count", count));
        return queryEntities(uri.toString(), User.class);
    }

    @Override
    public Result<User[]> getMediaLikes(String mediaId, int count) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/media/%s/likes", urlEncode(mediaId)),
                Parameter.as("count", count));
        return queryEntities(uri.toString(), User.class);
    }

    @Override
    public Result<Comment[]> getMediaComments(String mediaId, int count) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/media/%s/comments", urlEncode(mediaId)),
                Parameter.as("count", count));
        return queryEntities(uri.toString(), Comment.class);
    }

    @Override
    public Result<Comment[]> getCommentsNext(Pagination pagination) throws InstagramException {
        return queryEntities(pagination.getNextUrl(), Comment.class);
    }

    @Override
    public Result<User[]> getUsersNext(Pagination pagination) throws InstagramException {
        return queryEntities(pagination.getNextUrl(), User.class);
    }

    @Override
    public Result<Relationship> getRelationship(String userId) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/users/%s/relationship", urlEncode(userId)));
        return queryEntity(uri.toString(), Relationship.class);
    }

    @Override
    public Result<Media> getMedia(String mediaId) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/media/" + urlEncode(mediaId));
        return queryEntity(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getRecentMediaForUser(String userId, Parameter... params) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/users/%s/media/recent", urlEncode(userId)), params);
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getUserFeed(Parameter... params) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/users/self/feed", params);
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getRecentMediaForTag(String tag, Parameter... params) throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/tags/%s/media/recent", urlEncode(tag)), params);
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getRecentMediaAtLocation(String locationId, Parameter... params)
            throws InstagramException {
        URIBuilder uri = createUriBuilder(
                String.format("https://api.instagram.com/v1/locations/%s/media/recent", urlEncode(locationId)),
                params);
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getRecentMediaNearby(double latitude, double longitude, Integer radiusMeters,
            Parameter... params) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/media/search", params);
        uri.addParameter("lat", String.format("%.5f", latitude));
        uri.addParameter("lng", String.format("%.5f", longitude));
        if (radiusMeters != null && radiusMeters > 0)
            uri.addParameter("distance", radiusMeters.toString());
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Media[]> getMediaNext(Pagination pagination) throws InstagramException {
        return queryEntities(pagination.getNextUrl(), Media.class);
    }

    @Override
    public Result<Location> getLocation(String locationId) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/locations/" + urlEncode(locationId));
        return queryEntity(uri.toString(), Location.class);
    }

    @Override
    public Result<Location[]> getLocationsNearby(double latitude, double longitude, Integer radiusKm,
            Parameter... params) throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/locations/search", params);
        uri.addParameter("lat", String.format("%.5f", latitude));
        uri.addParameter("lng", String.format("%.5f", longitude));
        if (radiusKm != null && radiusKm > 0)
            uri.addParameter("distance", radiusKm.toString());
        return queryEntities(uri.toString(), Location.class);
    }

    @Override
    public Result<Location[]> getLocationsForFoursquareVenue(String venueId, Parameter... params)
            throws InstagramException {
        URIBuilder uri = createUriBuilder("https://api.instagram.com/v1/locations/search", params);
        uri.addParameter("foursquare_v2_id", venueId); // Assume v2 id.

        return queryEntities(uri.toString(), Location.class);
    }

    @Override
    public Result<Location[]> getLocationsNext(Pagination pagination) throws InstagramException {
        return queryEntities(pagination.getNextUrl(), Location.class);
    }

    @Override
    public void addComment(String mediaId, String comment) throws InstagramException {
        if (mediaId == null)
            throw new IllegalArgumentException("Media ID is required");

        HttpPost post = new HttpPost(
                String.format("https://api.instagram.com/v1/media/%s/comments", urlEncode(mediaId)));

        List<NameValuePair> params = new ArrayList<NameValuePair>(2);
        params.add(new BasicNameValuePair("text", comment));
        params.add(new BasicNameValuePair("access_token", accessToken));

        try {
            post.setEntity(new UrlEncodedFormEntity(params));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unexpected error building Instagram request", e);
        }

        Result<?> result = requestEntity(post, null, true);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error adding comment to photo", post.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteComment(String mediaId, String commentId) throws InstagramException {
        if (mediaId == null || commentId == null)
            throw new IllegalArgumentException("Media ID and Comment ID are required");

        HttpDelete delete = new HttpDelete(
                String.format("https://api.instagram.com/v1/media/%s/comments/%s?access_token=%s", mediaId,
                        commentId, accessToken));

        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting comment from photo", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void likeMedia(String mediaId) throws InstagramException {
        if (mediaId == null)
            throw new IllegalArgumentException("Media ID is required");

        HttpPost post = new HttpPost(String.format("https://api.instagram.com/v1/media/%s/likes", mediaId));
        List<NameValuePair> params = new ArrayList<NameValuePair>(1);
        params.add(new BasicNameValuePair("access_token", accessToken));

        try {
            post.setEntity(new UrlEncodedFormEntity(params));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unexpected error building Instagram request", e);
        }

        Result<?> result = requestEntity(post, null, true);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error liking photo", post.getURI().toString(), null, result.getMeta(),
                    null);
    }

    @Override
    public void unlikeMedia(String mediaId) throws InstagramException {
        if (mediaId == null)
            throw new IllegalArgumentException("Media ID is required");

        HttpDelete delete = new HttpDelete(
                String.format("https://api.instagram.com/v1/media/%s/likes?access_token=%s", mediaId, accessToken));

        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error unliking photo", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public Result<Relationship> relationshipAction(String userId, RelationshipAction action)
            throws InstagramException {
        if (userId == null || action == null)
            throw new IllegalArgumentException("User ID and action are required");

        HttpPost post = new HttpPost(String.format("https://api.instagram.com/v1/users/%s/relationship", userId));
        List<NameValuePair> params = new ArrayList<NameValuePair>(2);
        params.add(new BasicNameValuePair("action", action.getCommand()));
        params.add(new BasicNameValuePair("access_token", accessToken));

        try {
            post.setEntity(new UrlEncodedFormEntity(params));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unexpected error building Instagram request", e);
        }

        return requestEntity(post, Relationship.class, true);
    }

    @Override
    public Result<Media[]> getRecentMediaForGeoSubscription(String geoSubscriptionId) throws InstagramException {
        // https://api.instagram.com/v1/geographies/{geography id}/media/recent?client_id=CLIENT-ID
        URIBuilder uri = createUriBuilderNoAuth(String
                .format("https://api.instagram.com/v1/geographies/%s/media/recent", urlEncode(geoSubscriptionId)),
                Parameter.as("client_id", clientId));
        return queryEntities(uri.toString(), Media.class);
    }

    @Override
    public Result<Subscription> createUserSubscription(String verifyToken, String callbackUrl)
            throws InstagramException {
        return createSubscription("user", null, "media", verifyToken, callbackUrl);
    }

    @Override
    public Result<Subscription> createTagSubscription(String tag, String verifyToken, String callbackUrl)
            throws InstagramException {
        return createSubscription("tag", tag, "media", verifyToken, callbackUrl);
    }

    @Override
    public Result<Subscription> createLocationSubscription(String locationId, String verifyToken,
            String callbackUrl) throws InstagramException {
        return createSubscription("location", locationId, "media", verifyToken, callbackUrl);
    }

    @Override
    public Result<Subscription> createGeoSubscription(double latitude, double longitude, int radius,
            String verifyToken, String callbackUrl) throws InstagramException {
        HttpPost post = new HttpPost("https://api.instagram.com/v1/subscriptions/");
        List<NameValuePair> params = new ArrayList<NameValuePair>(9);
        params.add(new BasicNameValuePair("client_id", clientId));
        params.add(new BasicNameValuePair("client_secret", clientSecret));
        params.add(new BasicNameValuePair("object", "geography"));
        params.add(new BasicNameValuePair("aspect", "media"));
        params.add(new BasicNameValuePair("lat", String.format("%.5f", latitude)));
        params.add(new BasicNameValuePair("lng", String.format("%.5f", longitude)));
        params.add(new BasicNameValuePair("radius", Integer.toString(radius)));
        params.add(new BasicNameValuePair("verify_token", verifyToken));
        params.add(new BasicNameValuePair("callback_url", callbackUrl));

        try {
            post.setEntity(new UrlEncodedFormEntity(params));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unexpected error building Instagram request", e);
        }

        return requestEntity(post, Subscription.class, false);
    }

    @Override
    public Result<Subscription[]> getSubscriptions() throws InstagramException {
        URIBuilder uri = createUriBuilderNoAuth("https://api.instagram.com/v1/subscriptions",
                Parameter.as("client_id", clientId), Parameter.as("client_secret", clientSecret));
        return queryEntities(uri.toString(), Subscription.class);
    }

    @Override
    public void deleteSubscription(String subscriptionId) throws InstagramException {
        HttpDelete delete = new HttpDelete(
                String.format("https://api.instagram.com/v1/subscriptions?id=%s&client_id=%s&client_secret=%s",
                        subscriptionId, clientId, clientSecret));

        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting subscription", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteUserSubscription() throws InstagramException {
        HttpDelete delete = new HttpDelete(String.format(
                "https://api.instagram.com/v1/subscriptions?object=user&client_id=%s&client_secret=%s", clientId,
                clientSecret));
        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting user subscription", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteTagSubscriptions() throws InstagramException {
        HttpDelete delete = new HttpDelete(
                String.format("https://api.instagram.com/v1/subscriptions?object=tag&client_id=%s&client_secret=%s",
                        clientId, clientSecret));
        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting tag subscriptions", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteLocationSubscriptions() throws InstagramException {
        HttpDelete delete = new HttpDelete(String.format(
                "https://api.instagram.com/v1/subscriptions?object=location&client_id=%s&client_secret=%s",
                clientId, clientSecret));
        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting location subscription", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteGeoSubscriptions() throws InstagramException {
        HttpDelete delete = new HttpDelete(String.format(
                "https://api.instagram.com/v1/subscriptions?object=geographytag&client_id=%s&client_secret=%s",
                clientId, clientSecret));
        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting geo subscription", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    @Override
    public void deleteAllSubscriptions() throws InstagramException {
        HttpDelete delete = new HttpDelete(
                String.format("https://api.instagram.com/v1/subscriptions?object=all&client_id=%s&client_secret=%s",
                        clientId, clientSecret));
        Result<?> result = requestEntity(delete, null, false);
        if (result != null && result.getMeta() != null && !result.getMeta().isSuccess())
            throw createInstagramException("Error deleting subscriptions", delete.getURI().toString(), null,
                    result.getMeta(), null);
    }

    private Result<Subscription> createSubscription(String object, String objectId, String aspect,
            String verifyToken, String callbackUrl) throws InstagramException {
        HttpPost post = new HttpPost("https://api.instagram.com/v1/subscriptions/");
        List<NameValuePair> params = new ArrayList<NameValuePair>(7);
        params.add(new BasicNameValuePair("client_id", clientId));
        params.add(new BasicNameValuePair("client_secret", clientSecret));
        params.add(new BasicNameValuePair("object", object));
        if (objectId != null)
            params.add(new BasicNameValuePair("object_id", objectId));
        params.add(new BasicNameValuePair("aspect", aspect));
        params.add(new BasicNameValuePair("verify_token", verifyToken));
        params.add(new BasicNameValuePair("callback_url", callbackUrl));

        try {
            post.setEntity(new UrlEncodedFormEntity(params));
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("Unexpected error building Instagram request", e);
        }

        return requestEntity(post, Subscription.class, false);
    }

    private <T> Result<T> queryEntity(String url, Class<T> type) throws InstagramException {
        HttpGet get = new HttpGet(url);
        return requestEntity(get, type, false);
    }

    private void setEnforceHeader(HttpRequestBase method) {
        if (!isSignedHeaderEnabled())
            return;

        if (clientSecret == null)
            throw new IllegalStateException("Client secret it required to use signed header");

        if (clientIps == null || clientIps.length() == 0)
            throw new IllegalStateException("Client IP(s) required to use signed header");

        try {
            SecretKeySpec signingKey = new SecretKeySpec(getClientSecret().getBytes(), HMAC_SHA256_ALGO);

            Mac mac = Mac.getInstance(HMAC_SHA256_ALGO);
            mac.init(signingKey);

            // Compute the hmac on IP address.
            byte[] rawHmac = mac.doFinal(clientIps.getBytes());

            String digest = Hex.encodeHexString(rawHmac);

            method.setHeader("X-Insta-Forwarded-For", String.format("%s|%s", clientIps, digest));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("Unexpected error creating signed header using HMAC-SHA256", e);
        } catch (InvalidKeyException e) {
            throw new IllegalStateException("Unexpected error creating signed header using HMAC-SHA256", e);
        }
    }

    private <T> Result<T> requestEntity(HttpRequestBase method, Class<T> type, boolean signableRequest)
            throws InstagramException {
        method.getParams().setParameter("http.useragent", "Instagram4j/1.0");

        JsonParser jp = null;
        HttpResponse response = null;
        ResultMeta meta = null;

        try {
            method.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");
            if (signableRequest)
                setEnforceHeader(method);

            HttpClient client = new DefaultHttpClient();
            client.getParams().setParameter(AllClientPNames.CONNECTION_TIMEOUT, 15000);
            client.getParams().setParameter(AllClientPNames.SO_TIMEOUT, 30000);

            if (LOG.isDebugEnabled())
                LOG.debug(String.format("Requesting entity entry point %s, method %s", method.getURI().toString(),
                        method.getMethod()));

            autoThrottle();

            response = client.execute(method);

            jp = createParser(response, method);

            JsonToken tok = jp.nextToken();
            if (tok != JsonToken.START_OBJECT) {
                if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
                    throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                            null, null);

                throw createInstagramException("Invalid response format from Instagram API",
                        method.getURI().toString(), response, null, null);
            }

            T data = null;

            while (true) {
                tok = jp.nextValue();
                if (tok == JsonToken.START_ARRAY) {
                    throw createInstagramException("Unexpected array in entity response " + jp.getCurrentName(),
                            method.getURI().toString(), response, meta, null);
                } else if (tok == JsonToken.START_OBJECT) {
                    // Should be "data" or "meta"
                    String name = jp.getCurrentName();
                    if ("meta".equals(name))
                        meta = jp.readValueAs(ResultMeta.class);
                    else if ("data".equals(name)) {
                        if (type != null)
                            data = jp.readValueAs(type);
                        else
                            jp.readValueAs(Map.class); // Consume & ignore
                    } else
                        throw createInstagramException("Unexpected field name " + name, method.getURI().toString(),
                                response, meta, null);
                } else
                    break;
            }

            if (data == null && meta == null && response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
                throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                        null, null);

            Result<T> result = new Result<T>(null, meta, data);
            setRateLimits(response, result);

            return result;
        } catch (JsonParseException e) {
            throw createInstagramException("Error parsing response from Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } catch (JsonProcessingException e) {
            throw createInstagramException("Error parsing response from Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } catch (IOException e) {
            throw createInstagramException("Error communicating with Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } finally {
            if (jp != null)
                try {
                    jp.close();
                } catch (IOException e) {
                }
            method.releaseConnection();
        }
    }

    private <T> Result<T[]> queryEntities(String url, Class<T> type) throws InstagramException {
        HttpGet get = new HttpGet(url);
        return requestEntities(get, type);
    }

    @SuppressWarnings("unchecked")
    private <T> Result<T[]> requestEntities(HttpRequestBase method, Class<T> type) throws InstagramException {
        method.getParams().setParameter("http.useragent", "Instagram4j/1.0");

        JsonParser jp = null;
        HttpResponse response = null;
        ResultMeta meta = null;

        try {
            method.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");
            setEnforceHeader(method);

            HttpClient client = new DefaultHttpClient();
            client.getParams().setParameter(AllClientPNames.CONNECTION_TIMEOUT, 15000);
            client.getParams().setParameter(AllClientPNames.SO_TIMEOUT, 30000);

            if (LOG.isDebugEnabled())
                LOG.debug(String.format("Requesting entities entry point %s, method %s", method.getURI().toString(),
                        method.getMethod()));

            autoThrottle();

            response = client.execute(method);

            jp = createParser(response, method);

            JsonToken tok = jp.nextToken();
            if (tok != JsonToken.START_OBJECT) {
                if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
                    throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                            null, null);

                throw createInstagramException("Invalid response format from Instagram API",
                        method.getURI().toString(), response, null, null);
            }

            Pagination pagination = null;
            T[] data = null;

            while (true) {
                tok = jp.nextValue();
                if (tok == JsonToken.START_ARRAY) {
                    // Should be "data"
                    String name = jp.getCurrentName();
                    if (!"data".equals(name))
                        throw createInstagramException("Unexpected field name " + name, method.getURI().toString(),
                                response, meta, null);

                    List<T> items = new ArrayList<T>();

                    tok = jp.nextToken();
                    if (tok == JsonToken.START_OBJECT) {
                        if (type != null) {
                            T item;
                            while ((item = jp.readValueAs(type)) != null)
                                items.add(item);
                        } else
                            jp.readValueAs(Map.class); // Consume & ignore
                    }

                    data = (T[]) Array.newInstance(type, items.size());
                    System.arraycopy(items.toArray(), 0, data, 0, items.size());
                } else if (tok == JsonToken.START_OBJECT) {
                    // Should be "pagination" or "meta"
                    String name = jp.getCurrentName();
                    if ("pagination".equals(name))
                        pagination = jp.readValueAs(Pagination.class);
                    else if ("meta".equals(name))
                        meta = jp.readValueAs(ResultMeta.class);
                    else
                        throw createInstagramException("Unexpected field name " + name, method.getURI().toString(),
                                response, meta, null);
                } else
                    break;
            }

            if (data == null && meta == null && response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
                throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                        null, null);

            Result<T[]> result = new Result<T[]>(pagination, meta, data);
            setRateLimits(response, result);

            return result;
        } catch (JsonParseException e) {
            throw createInstagramException("Error parsing response from Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } catch (JsonProcessingException e) {
            throw createInstagramException("Error parsing response from Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } catch (IOException e) {
            throw createInstagramException("Error communicating with Instagram: " + e.getMessage(),
                    method.getURI().toString(), response, meta, e);
        } finally {
            if (jp != null)
                try {
                    jp.close();
                } catch (IOException e) {
                }
            method.releaseConnection();
        }
    }

    private URIBuilder createUriBuilder(String uri, Parameter... params) {
        URIBuilder bldr = createUriBuilderNoAuth(uri, params);
        addAuth(bldr);
        return bldr;
    }

    private URIBuilder createUriBuilderNoAuth(String uri, Parameter... params) {
        try {
            URIBuilder bldr = new URIBuilder(uri);
            addParams(bldr, params);
            return bldr;
        } catch (URISyntaxException e) {
            throw new IllegalStateException("Unexpected error building URI for " + uri, e);
        }
    }

    private void setRateLimits(HttpResponse response, Result<?> result) {
        Header hdr = response.getFirstHeader("X-Ratelimit-Limit");
        if (hdr != null && hdr.getValue() != null)
            result.setRateLimit(Integer.valueOf(hdr.getValue()));

        hdr = response.getFirstHeader("X-Ratelimit-Remaining");
        if (hdr != null && hdr.getValue() != null)
            result.setRateLimitRemaining(Integer.valueOf(hdr.getValue()));
    }

    private String urlEncode(String s) {
        try {
            return ENCODER.encode(s);
        } catch (EncoderException e) {
            throw new IllegalStateException("Unexpected error encoding URL value", e);
        }
    }

    private void addAuth(URIBuilder uri) {
        if (accessToken != null)
            uri.addParameter("access_token", accessToken);
        else
            uri.addParameter("client_id", clientId);
    }

    private void addParams(URIBuilder uri, Parameter... params) {
        if (params != null && params.length != 0) {
            for (Parameter param : params)
                uri.addParameter(param.getName(), param.getValue());
        }
    }

    private JsonParser createParser(HttpResponse response, HttpRequestBase method)
            throws JsonParseException, IOException {
        HttpEntity entity = response.getEntity();
        // Always try to get response body since API will often return JSON for non-200 status codes.
        String responseBody = entity != null ? EntityUtils.toString(entity) : null;
        if (responseBody != null)
            responseBody = responseBody.trim();

        if (responseBody == null || responseBody.length() == 0) {
            if (response.getStatusLine().getStatusCode() >= 300)
                throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                        null, null);

            throw new InstagramException("Received empty response from Instagram");
        }

        // Since sometimes the Instagram API responds with non-JSON (e.g. "Oops, an error occurred."), sniff here.
        if (!responseBody.startsWith("{") && !responseBody.endsWith("}")) {
            if (response.getStatusLine().getStatusCode() >= 300)
                throw createInstagramException("Instagram request failed", method.getURI().toString(), response,
                        null, null);

            throw new InstagramException("Received unexpected response from Instagram: "
                    + (responseBody.length() > 100 ? responseBody.substring(0, 100) : responseBody));
        }

        return JSON_FACTORY.createParser(responseBody);
    }

    private InstagramException createInstagramException(String msg, String url, HttpResponse response,
            ResultMeta meta, Throwable t) {
        InstagramException e = t != null ? new InstagramException(msg, t) : new InstagramException(msg);
        e.setUrl(url);
        e.setMeta(meta);

        if (response != null && response.getStatusLine() != null) {
            e.setStatusCode(response.getStatusLine().getStatusCode());
            e.setStatusMessage(response.getStatusLine().getReasonPhrase());
        }
        return e;
    }

    private boolean autoThrottle() {
        if (autoThrottle > 0) {
            long now = System.currentTimeMillis();
            long sinceLast = now - lastCallTicks;
            if (sinceLast < autoThrottle) {
                try {
                    Thread.sleep(autoThrottle - sinceLast);
                    lastCallTicks = System.currentTimeMillis();
                } catch (InterruptedException e) {
                    return false;
                }
            } else
                lastCallTicks = now;
        }

        return true;
    }
}