com.palominolabs.crm.sf.rest.HttpApiClient.java Source code

Java tutorial

Introduction

Here is the source code for com.palominolabs.crm.sf.rest.HttpApiClient.java

Source

/*
 * Copyright 2013. Palomino Labs (http://palominolabs.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 com.palominolabs.crm.sf.rest;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.palominolabs.crm.sf.core.Id;
import com.palominolabs.crm.sf.core.SObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
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.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * A lightweight wrapper around HTTP that handles errors and makes requests to the SF Rest endpoint urls.
 */
@ThreadSafe
final class HttpApiClient {

    static final String API_VERSION = "32.0";
    static final TypeReference<List<ApiErrorImpl>> API_ERRORS_TYPE = new TypeReference<List<ApiErrorImpl>>() {
    };
    private static final String UPLOAD_CONTENT_TYPE = "application/json";

    private static final Logger logger = LoggerFactory.getLogger(HttpApiClient.class);

    @Nonnull
    private final String host;

    @Nonnull
    private final String oauthToken;

    @Nonnull
    private final ObjectMapper objectMapper;

    @Nonnull
    private final HttpClient client;

    HttpApiClient(@Nonnull String host, @Nonnull String oauthToken, @Nonnull ObjectMapper objectMapper,
            @Nonnull HttpClient client) {
        this.host = host;
        this.oauthToken = oauthToken;
        this.objectMapper = objectMapper;
        this.client = client;
    }

    @CheckForNull
    String describeGlobal() throws IOException {
        return executeGet("/sobjects/");
    }

    @CheckForNull
    String describeSObject(String sObjectType) throws IOException {
        return executeGet("/sobjects/" + sObjectType + "/describe");
    }

    @CheckForNull
    String basicSObjectInfo(String sObjectType) throws IOException {
        return executeGet("/sobjects/" + sObjectType);
    }

    @CheckForNull
    String create(SObject sObject) throws IOException {
        HttpPost post = new HttpPost(getUri("/sobjects/" + sObject.getType() + "/"));
        post.setEntity(getEntityForSObjectFieldsJson(sObject));
        post.addHeader("Content-Type", UPLOAD_CONTENT_TYPE);

        return executeRequestForString(post);
    }

    void delete(String sObjectType, Id id) throws IOException {
        executeRequestForString(new HttpDelete(getUri("/sobjects/" + sObjectType + "/" + id)));
    }

    @CheckForNull
    String query(String soql) throws IOException {
        return executeGet("/query", new BasicNameValuePair("q", soql));
    }

    @CheckForNull
    String queryMore(RestQueryLocator queryLocator) throws IOException {
        return executeGetForUri(getUriForPath(queryLocator.getContents()));
    }

    @CheckForNull
    String search(String sosl) throws IOException {
        return executeGet("/search", new BasicNameValuePair("q", sosl));
    }

    @CheckForNull
    String retrieve(String sObjectType, Id id, List<String> fields) throws IOException {
        return executeGet("/sobjects/" + sObjectType + "/" + id,
                new BasicNameValuePair("fields", StringUtils.join(fields, ",")));
    }

    void update(SObject sObject) throws IOException {
        HttpPatch patch = new HttpPatch(getUri("/sobjects/" + sObject.getType() + "/" + sObject.getId()));
        patch.setEntity(getEntityForSObjectFieldsJson(sObject));
        patch.addHeader("Content-Type", UPLOAD_CONTENT_TYPE);

        executeRequestForString(patch);
    }

    /**
     * @param sObject         sObject to upsert
     * @param externalIdField field name of external id field
     *
     * @return http status code
     *
     * @throws IOException on error
     */
    int upsert(SObject sObject, String externalIdField) throws IOException {
        HttpPatch patch = new HttpPatch(getUri("/sobjects/" + sObject.getType() + "/" + externalIdField + "/"
                + sObject.getField(externalIdField)));
        patch.setEntity(getEntityForSObjectFieldsJson(sObject));
        patch.addHeader("Content-Type", UPLOAD_CONTENT_TYPE);

        ProcessedResponse processedResponse = executeRequest(patch);
        return processedResponse.getHttpResponse().getStatusLine().getStatusCode();
    }

    @Nonnull
    private HttpEntity getEntityForSObjectFieldsJson(SObject sObject) throws IOException {
        return new StringEntity(getSObjectFieldsAsJson(sObject), "UTF-8");
    }

    @Nonnull
    private String getSObjectFieldsAsJson(@Nonnull SObject sObject) throws IOException {
        StringWriter writer = new StringWriter();
        JsonGenerator jsonGenerator = this.objectMapper.getFactory().createGenerator(writer);

        jsonGenerator.writeStartObject();

        for (Map.Entry<String, String> entry : sObject.getAllFields().entrySet()) {
            if (entry.getValue() == null) {
                jsonGenerator.writeNullField(entry.getKey());
            } else {
                jsonGenerator.writeStringField(entry.getKey(), entry.getValue());
            }
        }

        jsonGenerator.writeEndObject();
        jsonGenerator.close();

        writer.close();

        return writer.toString();
    }

    @CheckForNull
    private String executeGet(String pathFragment) throws IOException {
        return executeGetForUri(getUri(pathFragment));
    }

    @CheckForNull
    private String executeGet(String pathFragment, NameValuePair param) throws IOException {
        return executeGet(pathFragment, Arrays.asList(param));
    }

    @CheckForNull
    private String executeGet(String pathFragment, List<NameValuePair> params) throws IOException {
        return executeGetForUri(getUri(pathFragment, params));
    }

    @CheckForNull
    private String executeGetForUri(URI uri) throws IOException {
        return executeRequestForString(new HttpGet(uri));
    }

    @Nonnull
    private URI getUri(String pathFragment) throws IOException {
        return getUriForPath("/services/data/v" + API_VERSION + pathFragment);
    }

    @Nonnull
    private URI getUri(String pathFragment, List<NameValuePair> params) throws IOException {
        return getUriForPath("/services/data/v" + API_VERSION + pathFragment, params);
    }

    @Nonnull
    private URI getUriForPath(String path) throws IOException {
        try {
            return getUriBuilderForPath(path).build();
        } catch (URISyntaxException e) {
            throw new IOException("Couldn't create URI", e);
        }
    }

    @Nonnull
    private URI getUriForPath(String path, List<NameValuePair> params) throws IOException {

        URIBuilder b = getUriBuilderForPath(path);

        for (NameValuePair param : params) {
            b.addParameter(param.getName(), param.getValue());
        }

        try {
            return b.build();
        } catch (URISyntaxException e) {
            throw new IOException("Couldn't create URI", e);
        }
    }

    @Nonnull
    private URIBuilder getUriBuilderForPath(String path) {
        return new URIBuilder().setScheme("https").setHost(host).setPort(443).setPath(path);
    }

    @CheckForNull
    private String executeRequestForString(@Nonnull HttpUriRequest request) throws IOException {
        return executeRequest(request).getResponseBody();
    }

    @Nonnull
    private ProcessedResponse executeRequest(HttpUriRequest request) throws IOException {
        request.addHeader("Authorization", "OAuth " + this.oauthToken);
        HttpResponse response = this.client.execute(request);

        return new ProcessedResponse(response, checkResponse(request, response));
    }

    /**
     * @param request  the http request
     * @param response the http response
     *
     * @return response body. May be null.
     *
     * @throws IOException if the response indicates an error
     */
    @CheckForNull
    private String checkResponse(HttpUriRequest request, HttpResponse response) throws IOException {
        HttpEntity entity = response.getEntity();

        String responseBody;
        if (entity == null) {
            responseBody = null;
        } else {
            responseBody = EntityUtils.toString(entity);
        }

        throwApiExceptionIfInvalid(request.getURI().toString(), response, responseBody);
        return responseBody;
    }

    private void throwApiExceptionIfInvalid(@Nonnull String url, @Nonnull HttpResponse response,
            @Nullable String responseBody) throws IOException {
        StatusLine statusLine = response.getStatusLine();
        int statusCode = statusLine.getStatusCode();

        if (statusCode >= 200 && statusCode < 300) {
            return;
        }

        List<ApiError> errors;
        if (responseBody == null) {
            errors = Collections.emptyList();
        } else {
            try {
                errors = this.objectMapper.readValue(responseBody, API_ERRORS_TYPE);
            } catch (JsonParseException e) {
                logger.warn("Couldn't parse response <" + responseBody + ">", e);
                errors = Collections.emptyList();
            } catch (JsonMappingException e) {
                logger.warn("Couldn't parse response <" + responseBody + ">", e);
                errors = Collections.emptyList();
            }
        }

        switch (statusCode) {
        case 300:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "External ID already used");

        case 400:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Request could not be understood");

        case 401:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Invalid session ID or Oauth token");

        case 403:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Request refused; check permissions");

        case 404:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Resource could not be found");

        case 405:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Method not allowed for specified resource");

        case 415:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Request is not in a supported format for the resource and method");

        case 500:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Force.com error");
        default:
            throw new ApiException(url, statusCode, statusLine.getReasonPhrase(), errors, responseBody,
                    "Unclassified error");
        }
    }

    /**
     * A trivial holder around an HttpResponse and the String body.
     */
    @NotThreadSafe
    private static class ProcessedResponse {

        @Nonnull
        private final HttpResponse httpResponse;

        @CheckForNull
        private final String responseBody;

        private ProcessedResponse(@Nonnull HttpResponse httpResponse, @Nullable String responseBody) {
            this.httpResponse = httpResponse;
            this.responseBody = responseBody;
        }

        @Nonnull
        public HttpResponse getHttpResponse() {
            return httpResponse;
        }

        @CheckForNull
        public String getResponseBody() {
            return responseBody;
        }
    }
}