/** * 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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.XStreamException; import com.thoughtworks.xstream.converters.reflection.FieldDictionary; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; import com.thoughtworks.xstream.core.TreeMarshallingStrategy; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.naming.NoNameCoder; import com.thoughtworks.xstream.io.xml.CompactWriter; import com.thoughtworks.xstream.io.xml.XppDriver; import org.apache.camel.component.salesforce.SalesforceEndpointConfig; 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.dto.AnnotationFieldKeySorter; import org.apache.camel.component.salesforce.api.dto.RestError; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse; import org.apache.camel.component.salesforce.api.dto.composite.SObjectTree; import org.apache.camel.component.salesforce.api.dto.composite.SObjectTreeResponse; import org.apache.camel.component.salesforce.api.utils.DateTimeConverter; import org.apache.camel.component.salesforce.api.utils.JsonUtils; import org.apache.camel.component.salesforce.api.utils.Version; import org.apache.camel.component.salesforce.internal.PayloadFormat; import org.apache.camel.component.salesforce.internal.SalesforceSession; import org.apache.camel.util.ObjectHelper; import org.eclipse.jetty.client.api.ContentProvider; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DefaultCompositeApiClient extends AbstractClientBase implements CompositeApiClient { private static final Class[] ADDITIONAL_TYPES = new Class[] {SObjectTree.class, SObjectTreeResponse.class, SObjectBatchResponse.class}; private static final Logger LOG = LoggerFactory.getLogger(DefaultCompositeApiClient.class); private final PayloadFormat format; private ObjectMapper mapper; private final Map<Class<?>, ObjectReader> readers = new HashMap<>(); private final Map<Class<?>, ObjectWriter> writters = new HashMap<>(); private final XStream xStream; public DefaultCompositeApiClient(final SalesforceEndpointConfig configuration, final PayloadFormat format, final String version, final SalesforceSession session, final SalesforceHttpClient httpClient) throws SalesforceException { super(version, session, httpClient); this.format = format; if (configuration.getObjectMapper() != null) { mapper = configuration.getObjectMapper(); } else { mapper = JsonUtils.createObjectMapper(); } xStream = configureXStream(); } static XStream configureXStream() { final PureJavaReflectionProvider reflectionProvider = new PureJavaReflectionProvider( new FieldDictionary(new AnnotationFieldKeySorter())); final XppDriver hierarchicalStreamDriver = new XppDriver(new NoNameCoder()) { @Override public HierarchicalStreamWriter createWriter(final Writer out) { return new CompactWriter(out, getNameCoder()); } }; final XStream xStream = new XStream(reflectionProvider, hierarchicalStreamDriver); xStream.aliasSystemAttribute(null, "class"); xStream.ignoreUnknownElements(); XStreamUtils.addDefaultPermissions(xStream); xStream.registerConverter(new DateTimeConverter()); xStream.setMarshallingStrategy(new TreeMarshallingStrategy()); xStream.processAnnotations(ADDITIONAL_TYPES); return xStream; } @Override public void submitCompositeBatch(final SObjectBatch batch, final ResponseCallback<SObjectBatchResponse> callback) throws SalesforceException { checkCompositeBatchVersion(version, batch.getVersion()); final String url = versionUrl() + "composite/batch"; final Request post = createRequest(HttpMethod.POST, url); final ContentProvider content = serialize(batch, batch.objectTypes()); post.content(content); doHttpRequest(post, (response, exception) -> callback .onResponse(tryToReadResponse(SObjectBatchResponse.class, response), exception)); } @Override public void submitCompositeTree(final SObjectTree tree, final ResponseCallback<SObjectTreeResponse> callback) throws SalesforceException { final String url = versionUrl() + "composite/tree/" + tree.getObjectType(); final Request post = createRequest(HttpMethod.POST, url); final ContentProvider content = serialize(tree, tree.objectTypes()); post.content(content); doHttpRequest(post, (response, exception) -> callback .onResponse(tryToReadResponse(SObjectTreeResponse.class, response), exception)); } static void checkCompositeBatchVersion(final String configuredVersion, final Version batchVersion) throws SalesforceException { if (Version.create(configuredVersion).compareTo(batchVersion) < 0) { throw new SalesforceException("Component is configured with Salesforce API version " + configuredVersion + ", but the payload of the Composite API batch operation requires at least " + batchVersion, 0); } } Request createRequest(final HttpMethod method, final String url) { final Request request = getRequest(method, url); // setup authorization setAccessToken(request); if (format == PayloadFormat.JSON) { request.header(HttpHeader.CONTENT_TYPE, APPLICATION_JSON_UTF8); request.header(HttpHeader.ACCEPT, APPLICATION_JSON_UTF8); } else { // must be XML request.header(HttpHeader.CONTENT_TYPE, APPLICATION_XML_UTF8); request.header(HttpHeader.ACCEPT, APPLICATION_XML_UTF8); } request.header(HttpHeader.ACCEPT_CHARSET, StringUtil.__UTF8); return request; } <T> T fromJson(final Class<T> expectedType, final InputStream responseStream) throws IOException { return jsonReaderFor(expectedType).readValue(responseStream); } <T> T fromXml(final InputStream responseStream) { @SuppressWarnings("unchecked") final T read = (T) xStream.fromXML(responseStream); return read; } ObjectReader jsonReaderFor(final Class<?> type) { return Optional.ofNullable(readers.get(type)).orElseGet(() -> mapper.readerFor(type)); } ObjectWriter jsonWriterFor(final Object obj) { final Class<?> type = obj.getClass(); return Optional.ofNullable(writters.get(type)).orElseGet(() -> mapper.writerFor(type)); } ContentProvider serialize(final Object body, final Class<?>... additionalTypes) throws SalesforceException { final InputStream stream; if (format == PayloadFormat.JSON) { stream = toJson(body); } else { // must be XML stream = toXml(body, additionalTypes); } // input stream as entity content is needed for authentication retries return new InputStreamContentProvider(stream); } String servicesDataUrl() { return instanceUrl + "/services/data/"; } InputStream toJson(final Object obj) throws SalesforceException { byte[] jsonBytes; try { jsonBytes = jsonWriterFor(obj).writeValueAsBytes(obj); } catch (final JsonProcessingException e) { throw new SalesforceException("Unable to serialize given SObjectTree to JSON", e); } return new ByteArrayInputStream(jsonBytes); } InputStream toXml(final Object obj, final Class<?>... additionalTypes) { xStream.processAnnotations(additionalTypes); final ByteArrayOutputStream out = new ByteArrayOutputStream(); xStream.toXML(obj, out); return new ByteArrayInputStream(out.toByteArray()); } <T> Optional<T> tryToReadResponse(final Class<T> expectedType, final InputStream responseStream) { try { if (format == PayloadFormat.JSON) { return Optional.of(fromJson(expectedType, responseStream)); } else { // must be XML return Optional.of(fromXml(responseStream)); } } catch (XStreamException | IOException e) { LOG.warn("Unable to read response from the Composite API", e); return Optional.empty(); } finally { try { responseStream.close(); } catch (final IOException ignored) { } } } String versionUrl() { ObjectHelper.notNull(version, "version"); return servicesDataUrl() + "v" + version + "/"; } @Override protected SalesforceException createRestException(final Response response, final InputStream responseContent) { final List<RestError> errors; try { errors = readErrorsFrom(responseContent, format, mapper, xStream); } catch (IOException e) { return new SalesforceException("Unable to read error response", e); } final int status = response.getStatus(); if (status == HttpStatus.NOT_FOUND_404) { return new NoSuchSObjectException(errors); } final String reason = response.getReason(); return new SalesforceException("Unexpected error: " + reason, status); } @Override protected void setAccessToken(final Request request) { request.getHeaders().put("Authorization", "Bearer " + accessToken); } }