Java tutorial
/* * 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; } } }