/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.component.salesforce.internal.client; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URLEncoder; import java.util.List; import java.util.Map; import java.util.Optional; import com.fasterxml.jackson.databind.ObjectMapper; import com.thoughtworks.xstream.XStream; import org.apache.camel.component.salesforce.SalesforceHttpClient; import org.apache.camel.component.salesforce.api.NoSuchSObjectException; import org.apache.camel.component.salesforce.api.SalesforceException; import org.apache.camel.component.salesforce.api.SalesforceMultipleChoicesException; import org.apache.camel.component.salesforce.api.TypeReferences; import org.apache.camel.component.salesforce.api.dto.RestError; import org.apache.camel.component.salesforce.api.utils.JsonUtils; import org.apache.camel.component.salesforce.internal.PayloadFormat; import org.apache.camel.component.salesforce.internal.SalesforceSession; import org.apache.camel.component.salesforce.internal.dto.RestChoices; import org.apache.camel.component.salesforce.internal.dto.RestErrors; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.URISupport; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.util.InputStreamContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.StringUtil; public class DefaultRestClient extends AbstractClientBase implements RestClient { private static final String SERVICES_DATA = "/services/data/"; private static final String TOKEN_HEADER = "Authorization"; private static final String TOKEN_PREFIX = "Bearer "; private static final String SERVICES_APEXREST = "/services/apexrest/"; protected PayloadFormat format; private ObjectMapper objectMapper; private XStream xStream; public DefaultRestClient(final SalesforceHttpClient httpClient, final String version, final PayloadFormat format, final SalesforceSession session) throws SalesforceException { super(version, session, httpClient); this.format = format; // initialize error parsers for JSON and XML this.objectMapper = JsonUtils.createObjectMapper(); this.xStream = new XStream(); xStream.processAnnotations(RestErrors.class); xStream.processAnnotations(RestChoices.class); xStream.ignoreUnknownElements(); XStreamUtils.addDefaultPermissions(xStream); } @Override protected void doHttpRequest(Request request, ClientResponseCallback callback) { // set standard headers for all requests final String contentType = PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8; request.header(HttpHeader.ACCEPT, contentType); request.header(HttpHeader.ACCEPT_CHARSET, StringUtil.__UTF8); // request content type and charset is set by the request entity super.doHttpRequest(request, callback); } @Override protected SalesforceException createRestException(Response response, InputStream responseContent) { // get status code and reason phrase final int statusCode = response.getStatus(); String reason = response.getReason(); if (reason == null || reason.isEmpty()) { reason = HttpStatus.getMessage(statusCode); } // try parsing response according to format try { if (responseContent != null && responseContent.available() > 0) { final List<String> choices; // return list of choices as error message for 300 if (statusCode == HttpStatus.MULTIPLE_CHOICES_300) { if (PayloadFormat.JSON.equals(format)) { choices = objectMapper.readValue(responseContent, TypeReferences.STRING_LIST_TYPE); } else { RestChoices restChoices = new RestChoices(); xStream.fromXML(responseContent, restChoices); choices = restChoices.getUrls(); } return new SalesforceMultipleChoicesException(reason, statusCode, choices); } else { final List<RestError> restErrors = readErrorsFrom(responseContent, format, objectMapper, xStream); if (statusCode == HttpStatus.NOT_FOUND_404) { return new NoSuchSObjectException(restErrors); } return new SalesforceException(restErrors, statusCode); } } } catch (IOException e) { // log and ignore String msg = "Unexpected Error parsing " + format + " error response body + [" + responseContent + "] : " + e.getMessage(); log.warn(msg, e); } catch (RuntimeException e) { // log and ignore String msg = "Unexpected Error parsing " + format + " error response body + [" + responseContent + "] : " + e.getMessage(); log.warn(msg, e); } // just report HTTP status info return new SalesforceException("Unexpected error: " + reason + ", with content: " + responseContent, statusCode); } @Override public void approval(final InputStream request, final ResponseCallback callback) { final Request post = getRequest(HttpMethod.POST, versionUrl() + "process/approvals/"); // authorization setAccessToken(post); // input stream as entity content post.content(new InputStreamContentProvider(request)); post.header(HttpHeader.CONTENT_TYPE, PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8); doHttpRequest(post, new DelegatingClientCallback(callback)); } @Override public void approvals(final ResponseCallback callback) { final Request get = getRequest(HttpMethod.GET, versionUrl() + "process/approvals/"); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getVersions(final ResponseCallback callback) { Request get = getRequest(HttpMethod.GET, servicesDataUrl()); // does not require authorization token doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getResources(ResponseCallback callback) { Request get = getRequest(HttpMethod.GET, versionUrl()); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getGlobalObjects(ResponseCallback callback) { Request get = getRequest(HttpMethod.GET, sobjectsUrl("")); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getBasicInfo(String sObjectName, ResponseCallback callback) { Request get = getRequest(HttpMethod.GET, sobjectsUrl(sObjectName + "/")); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getDescription(String sObjectName, ResponseCallback callback) { Request get = getRequest(HttpMethod.GET, sobjectsUrl(sObjectName + "/describe/")); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void getSObject(String sObjectName, String id, String[] fields, ResponseCallback callback) { // parse fields if set String params = ""; if (fields != null && fields.length > 0) { StringBuilder fieldsValue = new StringBuilder("?fields="); for (int i = 0; i < fields.length; i++) { fieldsValue.append(fields[i]); if (i < (fields.length - 1)) { fieldsValue.append(','); } } params = fieldsValue.toString(); } Request get = getRequest(HttpMethod.GET, sobjectsUrl(sObjectName + "/" + id + params)); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void createSObject(String sObjectName, InputStream sObject, ResponseCallback callback) { // post the sObject final Request post = getRequest(HttpMethod.POST, sobjectsUrl(sObjectName)); // authorization setAccessToken(post); // input stream as entity content post.content(new InputStreamContentProvider(sObject)); post.header(HttpHeader.CONTENT_TYPE, PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8); doHttpRequest(post, new DelegatingClientCallback(callback)); } @Override public void updateSObject(String sObjectName, String id, InputStream sObject, ResponseCallback callback) { final Request patch = getRequest("PATCH", sobjectsUrl(sObjectName + "/" + id)); // requires authorization token setAccessToken(patch); // input stream as entity content patch.content(new InputStreamContentProvider(sObject)); patch.header(HttpHeader.CONTENT_TYPE, PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8); doHttpRequest(patch, new DelegatingClientCallback(callback)); } @Override public void deleteSObject(String sObjectName, String id, ResponseCallback callback) { final Request delete = getRequest(HttpMethod.DELETE, sobjectsUrl(sObjectName + "/" + id)); // requires authorization token setAccessToken(delete); doHttpRequest(delete, new DelegatingClientCallback(callback)); } @Override public void getSObjectWithId(String sObjectName, String fieldName, String fieldValue, ResponseCallback callback) { final Request get = getRequest(HttpMethod.GET, sobjectsExternalIdUrl(sObjectName, fieldName, fieldValue)); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void upsertSObject(String sObjectName, String fieldName, String fieldValue, InputStream sObject, ResponseCallback callback) { final Request patch = getRequest("PATCH", sobjectsExternalIdUrl(sObjectName, fieldName, fieldValue)); // requires authorization token setAccessToken(patch); // input stream as entity content patch.content(new InputStreamContentProvider(sObject)); // TODO will the encoding always be UTF-8?? patch.header(HttpHeader.CONTENT_TYPE, PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8); doHttpRequest(patch, new DelegatingClientCallback(callback)); } @Override public void deleteSObjectWithId(String sObjectName, String fieldName, String fieldValue, ResponseCallback callback) { final Request delete = getRequest(HttpMethod.DELETE, sobjectsExternalIdUrl(sObjectName, fieldName, fieldValue)); // requires authorization token setAccessToken(delete); doHttpRequest(delete, new DelegatingClientCallback(callback)); } @Override public void getBlobField(String sObjectName, String id, String blobFieldName, ResponseCallback callback) { final Request get = getRequest(HttpMethod.GET, sobjectsUrl(sObjectName + "/" + id + "/" + blobFieldName)); // TODO this doesn't seem to be required, the response is always the content binary stream //get.header(HttpHeader.ACCEPT_ENCODING, "base64"); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void query(String soqlQuery, ResponseCallback callback) { try { String encodedQuery = urlEncode(soqlQuery); final Request get = getRequest(HttpMethod.GET, versionUrl() + "query/?q=" + encodedQuery); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } catch (UnsupportedEncodingException e) { String msg = "Unexpected error: " + e.getMessage(); callback.onResponse(null, new SalesforceException(msg, e)); } } @Override public void queryMore(String nextRecordsUrl, ResponseCallback callback) { final Request get = getRequest(HttpMethod.GET, instanceUrl + nextRecordsUrl); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } @Override public void queryAll(String soqlQuery, ResponseCallback callback) { try { String encodedQuery = urlEncode(soqlQuery); final Request get = getRequest(HttpMethod.GET, versionUrl() + "queryAll/?q=" + encodedQuery); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } catch (UnsupportedEncodingException e) { String msg = "Unexpected error: " + e.getMessage(); callback.onResponse(null, new SalesforceException(msg, e)); } } @Override public void search(String soslQuery, ResponseCallback callback) { try { String encodedQuery = urlEncode(soslQuery); final Request get = getRequest(HttpMethod.GET, versionUrl() + "search/?q=" + encodedQuery); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(callback)); } catch (UnsupportedEncodingException e) { String msg = "Unexpected error: " + e.getMessage(); callback.onResponse(null, new SalesforceException(msg, e)); } } @Override public void apexCall(String httpMethod, String apexUrl, Map<String, Object> queryParams, InputStream requestDto, ResponseCallback callback) { // create APEX call request final Request request; try { request = getRequest(httpMethod, apexCallUrl(apexUrl, queryParams)); // set request SObject and content type if (requestDto != null) { request.content(new InputStreamContentProvider(requestDto)); request.header(HttpHeader.CONTENT_TYPE, PayloadFormat.JSON.equals(format) ? APPLICATION_JSON_UTF8 : APPLICATION_XML_UTF8); } // requires authorization token setAccessToken(request); doHttpRequest(request, new DelegatingClientCallback(callback)); } catch (UnsupportedEncodingException e) { String msg = "Unexpected error: " + e.getMessage(); callback.onResponse(null, new SalesforceException(msg, e)); } catch (URISyntaxException e) { String msg = "Unexpected error: " + e.getMessage(); callback.onResponse(null, new SalesforceException(msg, e)); } } private String apexCallUrl(String apexUrl, Map<String, Object> queryParams) throws UnsupportedEncodingException, URISyntaxException { if (queryParams != null && !queryParams.isEmpty()) { apexUrl = URISupport.appendParametersToURI(apexUrl, queryParams); } return instanceUrl + SERVICES_APEXREST + apexUrl; } @Override public void recent(final Integer limit, final ResponseCallback responseCallback) { final String param = Optional.ofNullable(limit).map(v -> "?limit=" + v).orElse(""); final Request get = getRequest(HttpMethod.GET, versionUrl() + "recent/" + param); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(responseCallback)); } @Override public void limits(final ResponseCallback responseCallback) { final Request get = getRequest(HttpMethod.GET, versionUrl() + "limits/"); // requires authorization token setAccessToken(get); doHttpRequest(get, new DelegatingClientCallback(responseCallback)); } private String servicesDataUrl() { return instanceUrl + SERVICES_DATA; } private String versionUrl() { ObjectHelper.notNull(version, "version"); return servicesDataUrl() + "v" + version + "/"; } private String sobjectsUrl(String sObjectName) { ObjectHelper.notNull(sObjectName, "sObjectName"); return versionUrl() + "sobjects/" + sObjectName; } private String sobjectsExternalIdUrl(String sObjectName, String fieldName, String fieldValue) { if (fieldName == null || fieldValue == null) { throw new IllegalArgumentException("External field name and value cannot be NULL"); } try { String encodedValue = urlEncode(fieldValue); return sobjectsUrl(sObjectName + "/" + fieldName + "/" + encodedValue); } catch (UnsupportedEncodingException e) { String msg = "Unexpected error: " + e.getMessage(); throw new IllegalArgumentException(msg, e); } } protected void setAccessToken(Request request) { // replace old token request.getHeaders().put(TOKEN_HEADER, TOKEN_PREFIX + accessToken); } private String urlEncode(String query) throws UnsupportedEncodingException { String encodedQuery = URLEncoder.encode(query, StringUtil.__UTF8); // URLEncoder likes to use '+' for spaces encodedQuery = encodedQuery.replace("+", "%20"); return encodedQuery; } private static class DelegatingClientCallback implements ClientResponseCallback { private final ResponseCallback callback; DelegatingClientCallback(ResponseCallback callback) { this.callback = callback; } @Override public void onResponse(InputStream response, SalesforceException ex) { callback.onResponse(response, ex); } } }