com.keydap.sparrow.SparrowClient.java Source code

Java tutorial

Introduction

Here is the source code for com.keydap.sparrow.SparrowClient.java

Source

/*
 * Copyright (c) 2016 Keydap Software.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * See LICENSE file for details.
 */
package com.keydap.sparrow;

import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.http.HttpStatus.SC_NOT_MODIFIED;
import static org.apache.http.HttpStatus.SC_NO_CONTENT;
import static org.apache.http.HttpStatus.SC_OK;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;

import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.keydap.sparrow.auth.Authenticator;

/**
 * A client for any SCIM v2.0 compliant server.
 * 
 * @author Kiran Ayyagari (kayyagari@keydap.com)
 */
@SuppressWarnings("all")
public class SparrowClient {

    /** the base API URL of the SCIM server e.g https://sparrow.keydap.com/v2 */
    private String baseApiUrl;

    /** the base API URL of the OAuth server e.g https://sparrow.keydap.com/oauth2 */
    private String baseOauthUrl;

    /** the authenticator instance */
    private Authenticator authenticator;

    /** HTTP client builder */
    private HttpClientBuilder builder;

    /** the main HTTP client instance used for communicating with the server */
    private CloseableHttpClient client;

    /** SCIM entity serializer and deserializer */
    private Gson serializer;

    /** the logger instance */
    private static final Logger LOG = LoggerFactory.getLogger(SparrowClient.class);

    /** map holding <endpoint-entityClass> tuples */
    private Map<String, Class<?>> endpointClassMap = new HashMap<String, Class<?>>();

    /** map holding <schemaId-entityClass> tuples */
    private Map<String, Class<?>> schemaIdClassMap = new HashMap<String, Class<?>>();

    /** map holding <entityClass-endpoint> tuples */
    private Map<Class<?>, String> classEndpointMap = new HashMap<Class<?>, String>();

    private Map<String, Set<Field>> endpointExtFieldMap = new HashMap<String, Set<Field>>();

    /** the MIME type for application/scim+json content */
    public static final ContentType MIME_TYPE = ContentType.create("application/scim+json", Consts.UTF_8);

    /** type for serializing a List of JsonObjects */
    private static final Type lstJsonObj = new TypeToken<List<JsonObject>>() {
    }.getType();

    private JsonParser parser = new JsonParser();

    /**
     * Creates an instance of the client
     * 
     * @param baseApiUrl the API URL of the SCIM server
     */
    public SparrowClient(String baseApiUrl) {
        this(baseApiUrl, (String) null);
    }

    /**
     * Creates an instance of the client
     * 
     * @param baseApiUrl the API URL of the SCIM server
     * @param baseOauthUrl the API URL of the Oauth server
     */
    public SparrowClient(String baseApiUrl, String baseOauthUrl) {
        this(baseApiUrl, baseOauthUrl, null);

    }

    /**
     * Creates an instance of the client
     * 
     * @param baseApiUrl the API URL of the SCIM server
     * @param authenticator authenticator instance, optional
     */
    public SparrowClient(String baseApiUrl, Authenticator authenticator) {
        this(baseApiUrl, null, authenticator);
    }

    /**
     * Creates an instance of the client
     * 
     * @param baseApiUrl the API URL of the SCIM server
     * @param baseOauthUrl the API URL of the Oauth server
     * @param authenticator authenticator instance, optional
     */
    public SparrowClient(String baseApiUrl, String baseOauthUrl, Authenticator authenticator) {
        this(baseApiUrl, baseOauthUrl, authenticator, null);
    }

    /**
     * Creates an instance of the client
     * 
     * @param baseApiUrl the API URL of the SCIM server
     * @param authenticator authenticator instance, optional
     * @param sslCtx the SSL context, mandatory only when the service is accessible over HTTPS
     */
    public SparrowClient(String baseApiUrl, String baseOauthUrl, Authenticator authenticator, SSLContext sslCtx) {
        this.baseApiUrl = baseApiUrl;
        this.baseOauthUrl = baseOauthUrl;

        // if authenticator is not given then use a null authenticator
        if (authenticator == null) {
            authenticator = new Authenticator() {
                public void saveHeaders(HttpResponse resp) {
                }

                public void authenticate(String baseUrl, CloseableHttpClient client) throws Exception {
                }

                public void addHeaders(HttpUriRequest req) {
                }
            };
        }

        this.authenticator = authenticator;

        boolean isHttps = baseApiUrl.toLowerCase().startsWith("https");

        builder = HttpClientBuilder.create().useSystemProperties();

        if (isHttps) {
            if (sslCtx == null) {
                LOG.warn(
                        "********************** No SSLContext instance is provided, creating a cstom SSLContext that trusts all certificates **********************");
                try {
                    sslCtx = SSLContext.getInstance("TLS");
                    sslCtx.init(null, new X509TrustManager[] { new AllowAllTrustManager() }, null);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }

                builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE);
            }

            builder.setSSLContext(sslCtx);
        }

        client = builder.build();

        GsonBuilder gb = new GsonBuilder();

        Type dt = new TypeToken<Date>() {
        }.getType();
        gb.registerTypeAdapter(dt, new DateTimeSerializer());

        serializer = gb.create();
    }

    /**
     * Registers the given Resource classes.
     * 
     * @param resCls one or more Resource classes
     */
    public void register(Class<?>... resCls) {
        for (Class<?> rc : resCls) {
            Resource res = rc.getAnnotation(Resource.class);
            if (res == null) {
                LOG.warn("Resource annotation is missing, ignoring class {}", rc.getName());
                continue;
            }

            String schemaId = res.schemaId();
            if (schemaId.trim().length() == 0) {
                String err = "Invalid schemaId in Resource annotation of class " + rc.getName();
                LOG.warn(err);
                throw new IllegalArgumentException(err);
            }

            String endpoint = res.endpoint();
            if (endpoint.trim().length() == 0) {
                String err = "Invalid endpoint in Resource annotation of class " + rc.getName();
                LOG.warn(err);
                throw new IllegalArgumentException(err);
            }

            schemaIdClassMap.put(schemaId, rc);
            endpointClassMap.put(endpoint, rc);
            classEndpointMap.put(rc, endpoint);

            // the fields representing extended schema
            Set<Field> extResFields = new HashSet<Field>();

            Field[] fields = rc.getDeclaredFields();
            for (Field f : fields) {
                Extension ext = f.getAnnotation(Extension.class);
                if (ext != null) {
                    f.setAccessible(true);
                    extResFields.add(f);
                }
            }

            if (!extResFields.isEmpty()) {
                endpointExtFieldMap.put(endpoint, extResFields);
            }
        }
    }

    /**
     * Performs authentication using the authenticator
     * 
     * @throws Exception when the authenticator throws any exception
     */
    public void authenticate() throws Exception {
        authenticator.authenticate(baseApiUrl, client);
    }

    /**
     * Adds the given resource
     * 
     * @param rs the resource
     * @return
     */
    public <T> Response<T> addResource(T rs) {
        Class resClas = rs.getClass();
        String endpoint = getEndpoint(resClas);
        HttpPost post = new HttpPost(baseApiUrl + endpoint);
        setBody(post, rs);
        return sendRawRequest(post, resClas);
    }

    /**
     * Replaces the given resource
     * 
     * @param id identifier of the resource to be replaced
     * @param rs the new resource with which old one will be replaced
     * @return
     */
    public <T> Response<T> replaceResource(String id, T rs) {
        return replaceResource(id, rs, null);
    }

    /**
     * Replaces the given resource 
     * 
     * @param id identifier of the resource to be replaced
     * @param rs the new resource with which old one will be replaced
     * @param ifNoneMatch the value to be set for If-Match header
     * @return
     */
    public <T> Response<T> replaceResource(String id, T rs, String ifNoneMatch) {
        Class resClas = rs.getClass();
        String endpoint = getEndpoint(resClas);
        HttpPut put = new HttpPut(baseApiUrl + endpoint + "/" + id);
        setIfMatch(put, ifNoneMatch);
        setBody(put, rs);
        return sendRawRequest(put, resClas);
    }

    /**
     * Modifies the selected resource
     * @param pr the modify(a.k.a patch) request
     * @return
     */
    public <T> Response<T> patchResource(PatchRequest pr) {
        Class resClas = pr.getResClass();
        String endpoint = getEndpoint(resClas);

        String url = baseApiUrl + endpoint + "/" + pr.getId();

        if (pr.getAttributes() != null) {
            String encoded;
            try {
                encoded = URLEncoder.encode(pr.getAttributes(), Consts.UTF_8.name());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            url += "?attributes=" + encoded;
        }

        HttpPatch patch = new HttpPatch(url);
        setIfMatch(patch, pr.getIfMatch());
        setBody(patch, pr);
        return sendRawRequest(patch, resClas);
    }

    /**
     * Deletes the selected resource
     * 
     * @param id identifier of the resource to be deleted
     * @param resourceType the type of the resource that is to be deleted
     * @return
     */
    public Response<Boolean> deleteResource(String id, Class resourceType) {
        String endpoint = getEndpoint(resourceType);
        HttpDelete delete = new HttpDelete(baseApiUrl + endpoint + "/" + id);
        Response<Boolean> resp = sendRawRequest(delete, resourceType);
        if (resp.getHttpCode() == SC_NO_CONTENT) {
            resp.setResource(true);
        } else {
            resp.setResource(false);
        }

        return resp;
    }

    /**
     * Fetches the resource specified by the given identifier
     * 
     * @param id identifier of the resource
     * @param resClas the type of the resource to be fetched
     * @return
     */
    public <T> Response<T> getResource(String id, Class<T> resClas) {
        return getResource(id, null, resClas, true, "*");
    }

    /**
     * Fetches the resource specified by the given identifier.
     * If the version of the resource matched with the value of 
     * given value of ifNoneMatch parameter then the status result
     * 304 (NOT_MODIFIED) will be returned withoud any body
     * 
     * @param id identifier of the resource
     * @param ifNoneMatch the value of the resource's version
     * @param resClas the type of the resource to be fetched
     * @return
     */
    public <T> Response<T> getResource(String id, String ifNoneMatch, Class<T> resClas) {
        return getResource(id, ifNoneMatch, resClas, false, null);
    }

    /**
     * Same as {@link #getResource(String, String, Class, boolean, String...)} 
     * but without the If-Match header value
     * 
     * @see #getResource(String, String, Class, boolean, String...)
     */
    public <T> Response<T> getResource(String id, Class<T> resClas, boolean include, String... attributes) {
        return getResource(id, null, resClas, include, attributes);
    }

    /**
     * Fetches the resource specified by the given identifier.
     * If the version of the resource matched with the value of 
     * given value of ifNoneMatch parameter then the status result
     * 304 (NOT_MODIFIED) will be returned withoud any body.
     * 
     * The requested resource's attributes will be filtered based on the
     * specified attributes and the include flag.
     * 
     * @param id identifier of the resource
     * @param ifNoneMatch the value of the resource's version
     * @param resClas the type of the resource to be fetched
     * @param include the flag to determine if the attributes specified 
     *                should be included or excluded from the resource
     * @param attributes the attribute names
     * @return
     */
    public <T> Response<T> getResource(String id, String ifNoneMatch, Class<T> resClas, boolean include,
            String... attributes) {
        String endpoint = getEndpoint(resClas);
        StringBuilder sb = new StringBuilder(baseApiUrl + endpoint + "/" + id);
        if (attributes != null) {
            if (include) {
                sb.append("?attributes=");
            } else {
                sb.append("?excludedAttributes=");
            }

            int i = 0;
            for (; i < attributes.length - 1; i++) {
                String attr = attributes[i].trim();
                if (attr.length() > 0) {
                    sb.append(attr).append(',');
                }
            }
            String attr = attributes[i].trim();
            if (attr.length() > 0) {
                sb.append(attr);
            }
        }

        HttpGet get = new HttpGet(sb.toString());
        setIfMatch(get, ifNoneMatch);
        return sendRawRequest(get, resClas);
    }

    /**
     * Fetches all the resources of the specified type
     * 
     * @param resClas the type of the resources to be fetched
     * @return
     */
    public <T> SearchResponse<T> searchResource(Class<T> resClas) {
        String endpoint = getEndpoint(resClas);

        StringBuilder url = new StringBuilder(baseApiUrl);
        url.append(endpoint);

        HttpGet get = new HttpGet(url.toString());
        return sendSearchRequest(get, resClas);
    }

    /**
     * Fetches all the resources of the specified type matching the given search filter
     * 
     * @param filter the search filer
     * @param resClas the type of the resources to be searched
     * @return
     */
    public <T> SearchResponse<T> searchResource(String filter, Class<T> resClas) {
        String endpoint = getEndpoint(resClas);

        StringBuilder url = new StringBuilder(baseApiUrl);
        url.append(endpoint);

        if (filter != null) {
            url.append("/?filter=");
            try {
                url.append(URLEncoder.encode(filter, "utf-8"));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        HttpGet get = new HttpGet(url.toString());
        return sendSearchRequest(get, resClas);
    }

    /**
     * Fetches all the resources of the specified type matching the given search filter.
     * The resources will contain only the given set of attributes besides the mandatory
     * attributes, remaining attributes will not be received.
     * 
     * @param filter the search filer
     * @param resClas the type of the resources to be searched
     * @param attributes the attributes that the fetched resources should contain (besides the mandatory attributes)
     * @return
     */
    public <T> SearchResponse<T> searchResource(String filter, Class<T> resClas, String... attributes) {
        return searchResource(filter, resClas, true, attributes);
    }

    /**
     * Fetches all resources of the given resourcetype based on the given search request.
     * 
     * @param sr the search request
     * @param resClas the type of the resources to be searched
     * @return
     */
    public <T> SearchResponse<T> searchResource(SearchRequest sr, Class<T> resClas) {
        String endpoint = getEndpoint(resClas);
        return _searchResource(sr, endpoint, resClas);
    }

    /**
     * Fetches all resources based on the criteria present in the given search request
     * 
     * @param sr the search request
     * @return
     */
    public SearchResponse<Object> searchAll(SearchRequest sr) {
        return _searchResource(sr, "", null);
    }

    /**
     * Fetches all the resources of the specified type matching the given search filter.
     * Based on the include flag value the given attributes may be included or excluded.
     * The mandatory attributes will always be returned irrespective of the value of the
     * include flag's value.
     * 
     * @param filter the search filer
     * @param resClas the type of the resources to be searched
     * @param include flag that determines inclusion or exclusion of the given attributes
     * @param attributes the attributes that must be included or excluded
     * @return
     */
    public <T> SearchResponse<T> searchResource(String filter, Class<T> resClas, boolean include,
            String... attributes) {
        SearchRequest sr = new SearchRequest();
        sr.setFilter(filter);

        if (attributes != null) {
            int i = 0;
            StringBuilder sb = new StringBuilder();
            for (; i < attributes.length - 1; i++) {
                sb.append(attributes[i]).append(',');
            }
            sb.append(attributes[i]);

            if (include) {
                sr.setAttributes(sb.toString());
            } else {
                sr.setExcludedAttributes(sb.toString());
            }
        }

        return searchResource(sr, resClas);
    }

    /**
     * Fetches the service provider's configuration.
     * 
     * @return
     */
    public Response<JsonObject> getSrvProvConf() {
        HttpGet get = new HttpGet(baseApiUrl + "/ServiceProviderConfigs");
        return sendRawRequest(get, JsonObject.class);
    }

    /**
     * Fetches all resourcetypes supported by the service provider.
     * 
     * @return
     */
    public Response<List<JsonObject>> getResTypes() {
        HttpGet get = new HttpGet(baseApiUrl + "/ResourceTypes");
        Response resp = sendRawRequest(get, JsonElement.class);
        JsonElement je = (JsonElement) resp.getResource();
        if (je != null) {
            List<JsonObject> lst = serializer.fromJson(je, lstJsonObj);
            resp.setResource(lst);
        }

        return resp;
    }

    /**
     * Fetches the given resourcetype's definition
     * 
     * @param name name of the resourcetype to be fetched
     * @return
     */
    public Response<JsonObject> getResType(String name) {
        HttpGet get = new HttpGet(baseApiUrl + "/ResourceTypes/" + name);
        return sendRawRequest(get, JsonObject.class);
    }

    /**
     * Fetches all schemas supported by the service provider.
     * 
     * @return
     */
    public Response<List<JsonObject>> getSchemas() {
        HttpGet get = new HttpGet(baseApiUrl + "/Schemas");
        Response resp = sendRawRequest(get, JsonElement.class);
        JsonElement je = (JsonElement) resp.getResource();
        if (je != null) {
            List<JsonObject> lst = serializer.fromJson(je, lstJsonObj);
            resp.setResource(lst);
        }

        return resp;
    }

    /**
     * Fetches the schema with the given identifier
     * 
     * @param id identifier of the schema to be fetched
     * @return
     */
    public Response<JsonObject> getSchema(String id) {
        HttpGet get = new HttpGet(baseApiUrl + "/Schemas/" + id);
        return sendRawRequest(get, JsonObject.class);
    }

    private <T> SearchResponse<T> _searchResource(SearchRequest sr, String endpoint, Class<T> resClas) {
        StringBuilder url = new StringBuilder(baseApiUrl);
        url.append(endpoint).append("/.search");

        HttpPost post = new HttpPost(url.toString());
        String json = serializer.toJson(sr);
        StringEntity entity = new StringEntity(json, MIME_TYPE);
        post.setEntity(entity);

        return sendSearchRequest(post, resClas);
    }

    private <T> SearchResponse<T> sendSearchRequest(HttpUriRequest req, Class<T> resClas) {
        SearchResponse<T> result = new SearchResponse<T>();
        try {
            LOG.debug("Sending {} request to {}", req.getMethod(), req.getURI());
            authenticator.addHeaders(req);
            HttpResponse resp = client.execute(req);
            authenticator.saveHeaders(resp);
            StatusLine sl = resp.getStatusLine();
            int code = sl.getStatusCode();

            LOG.debug("Received status code {} from the request to {}", code, req.getURI());

            HttpEntity entity = resp.getEntity();
            String json = null;
            if (entity != null) {
                json = EntityUtils.toString(entity);
            }

            result.setHttpBody(json);
            result.setHttpCode(code);
            result.setHeaders(resp.getAllHeaders());

            // if it is success there will be response body to read
            if (code == 200) {
                if (json != null) { // DELETE will have no response body, so check for null

                    JsonObject obj = (JsonObject) new JsonParser().parse(json);
                    JsonElement je = obj.get("totalResults");
                    if (je != null) {
                        result.setTotalResults(je.getAsInt());
                    }

                    je = obj.get("startIndex");
                    if (je != null) {
                        result.setStartIndex(je.getAsInt());
                    }

                    je = obj.get("itemsPerPage");
                    if (je != null) {
                        result.setItemsPerPage(je.getAsInt());
                    }

                    je = obj.get("Resources"); // yes, the 'R' in resources must be upper case
                    if (je != null) {
                        JsonArray arr = je.getAsJsonArray();
                        Iterator<JsonElement> itr = arr.iterator();
                        List<T> resources = new ArrayList<T>();
                        while (itr.hasNext()) {
                            JsonObject r = (JsonObject) itr.next();
                            if (resClas != null) {
                                resources.add(unmarshal(r, resClas));
                            } else {
                                T rsObj = unmarshal(r);
                                if (rsObj == null) {
                                    LOG.warn(
                                            "No resgistered resource class found to deserialize the resource data {}",
                                            r);
                                } else {
                                    resources.add(rsObj);
                                }
                            }
                        }

                        if (!resources.isEmpty()) {
                            result.setResources(resources);
                        }
                    }
                }
            } else {
                if (json != null) {
                    Error error = serializer.fromJson(json, Error.class);
                    result.setError(error);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            LOG.warn("", e);
            result.setHttpCode(-1);
            Error err = new Error();

            err.setDetail(e.getMessage());
            result.setError(err);
        }
        return result;
    }

    private <T> T unmarshal(JsonObject json) throws Exception {
        JsonArray schemas = json.get("schemas").getAsJsonArray();
        Iterator<JsonElement> itr = schemas.iterator();

        T obj = null;

        while (itr.hasNext()) {
            String id = itr.next().getAsString();
            Class<?> rc = schemaIdClassMap.get(id);
            if (rc != null) {
                obj = (T) unmarshal(json, rc);
                break;
            }
        }

        return obj;
    }

    /**
     * Sends the given request to the server
     * 
     * @param req the HTTP request
     * @param resClas class of the resourcetype
     * @return
     */
    public <T> Response<T> sendRawRequest(HttpUriRequest req, Class<T> resClas) {
        Response<T> result = new Response<T>();
        try {
            authenticator.addHeaders(req);
            LOG.debug("Sending {} request to {}", req.getMethod(), req.getURI());
            HttpResponse resp = client.execute(req);
            authenticator.saveHeaders(resp);
            StatusLine sl = resp.getStatusLine();
            int code = sl.getStatusCode();

            LOG.debug("Received status code {} from the request to {}", code, req.getURI());
            HttpEntity entity = resp.getEntity();
            String json = null;
            if (entity != null) {
                json = EntityUtils.toString(entity);
            }

            result.setHttpCode(code);

            // if it is success there will be response body to read
            if (code == SC_OK || code == SC_CREATED || code == SC_NOT_MODIFIED) {
                if (json != null) { // some responses have no body, so check for null
                    T t = unmarshal(json, resClas);
                    result.setResource(t);
                }
            } else {
                if (json != null) {
                    Error error = serializer.fromJson(json, Error.class);
                    result.setError(error);
                }
            }

            result.setHttpBody(json);
            result.setHeaders(resp.getAllHeaders());
        } catch (Exception e) {
            LOG.warn("", e);
            result.setHttpCode(-1);
            Error err = new Error();

            err.setDetail(e.getMessage());
            result.setError(err);
        }

        return result;
    }

    private <T> void setBody(HttpEntityEnclosingRequestBase req, T rs) {
        String data = serialize(rs).toString();
        StringEntity entity = new StringEntity(data, MIME_TYPE);
        req.setEntity(entity);
    }

    /**
     * Serializes the given resourcetype instance
     * 
     * @param rs resourcetype's instance
     * @return
     */
    public <T> JsonObject serialize(T rs) {
        JsonObject json = (JsonObject) serializer.toJsonTree(rs);

        Resource r = rs.getClass().getAnnotation(Resource.class);
        if (r != null) {
            JsonArray schemas = new JsonArray();
            schemas.add(r.schemaId());

            Set<Field> extFields = endpointExtFieldMap.get(r.endpoint());
            if (extFields != null) {
                for (Field f : extFields) {
                    JsonElement je = json.remove(f.getName());
                    if (je != null) {
                        Extension extSchema = f.getAnnotation(Extension.class);
                        String schemaId = extSchema.value();
                        json.add(schemaId, je);
                        schemas.add(schemaId);
                    }
                }
            }

            json.add("schemas", schemas);
        }

        return json;
    }

    /**
     * Registers an application with the details present in the given request
     * 
     * @param appReq application request
     * @return a response containing RegisteredApp if successful or an Error when not
     */
    public Response<RegisteredApp> registerApp(RegisteredApp appReq) {
        Response<RegisteredApp> resp = addResource(appReq);
        return resp;
    }

    private String getEndpoint(Class resClas) {
        String ep = classEndpointMap.get(resClas);
        if (ep == null) {
            throw new IllegalArgumentException(
                    "There is no endpoint found with the given resource class. Resource class must be registered to avoid this error");
        }

        return ep;
    }

    private void setIfMatch(HttpRequestBase req, String ifMatch) {
        if (ifMatch != null) {
            req.setHeader("If-Match", ifMatch);
        }
    }

    private <T> T unmarshal(String json, Class<T> resClass) throws Exception {
        JsonElement je = parser.parse(json);
        if (!(je instanceof JsonObject)) {
            return (T) je;
        }

        return unmarshal((JsonObject) je, resClass);
    }

    private <T> T unmarshal(JsonObject jsonObj, Class<T> resClass) throws Exception {
        T t = serializer.fromJson(jsonObj, resClass);
        Set<Field> extFields = endpointExtFieldMap.get(classEndpointMap.get(resClass));
        if (extFields != null) {
            for (Field f : extFields) {
                Extension ext = f.getAnnotation(Extension.class);
                String schemaId = ext.value();
                JsonElement je = jsonObj.get(schemaId);
                if (je != null) {
                    Object extObj = serializer.fromJson(je, f.getType());
                    f.set(t, extObj);
                }
            }
        }

        return t;
    }

    public void normalizeKeys(Response resp) {
        String body = resp.getHttpBody();
        if (body == null) {
            return;
        }

        JsonObject json = (JsonObject) parser.parse(body);
        json = keysToLower(json);
        resp.setHttpBody(json.toString());
    }

    private JsonObject keysToLower(JsonObject obj) {
        JsonObject tmp = new JsonObject();
        for (String key : obj.keySet()) {
            JsonElement je = obj.get(key);
            if (key.contains(":")) {
                JsonObject ext = keysToLower((JsonObject) je);
                tmp.add(key, ext);
            } else {
                if (je.isJsonObject()) {
                    je = keysToLower((JsonObject) je);
                } else if (je.isJsonArray()) {
                    JsonArray arr = je.getAsJsonArray();
                    for (int i = 0; i < arr.size(); i++) {
                        JsonElement item = arr.get(i);
                        if (item.isJsonObject()) {
                            item = keysToLower((JsonObject) item);
                            arr.set(i, item);
                        }
                    }
                }

                tmp.add(key.toLowerCase(), je);
            }
        }

        return tmp;
    }
}