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.codahale.metrics.MetricRegistry; import com.codahale.metrics.Timer; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.palominolabs.crm.sf.core.Id; import com.palominolabs.crm.sf.core.SObject; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import java.io.IOException; import java.util.Iterator; import java.util.List; import static com.codahale.metrics.MetricRegistry.name; @ThreadSafe final class RestConnectionImpl implements RestConnection { private static final String ID_KEY = "Id"; private static final String ATTRIBUTES_KEY = "attributes"; private final ObjectReader objectReader; private final HttpApiClientProvider httpApiClientProvider; private final Timer createTimer; private final Timer deleteTimer; private final Timer describeGlobalTimer; private final Timer describeSObjectTimer; private final Timer queryTimer; private final Timer queryMoreTimer; private final Timer retrieveTimer; private final Timer searchTimer; private final Timer updateTimer; private final Timer basicSObjectInfoTimer; private final Timer upsertTimer; RestConnectionImpl(ObjectReader objectReader, HttpApiClientProvider httpApiClientProvider, MetricRegistry metricRegistry) { this.objectReader = objectReader; this.httpApiClientProvider = httpApiClientProvider; createTimer = metricRegistry.timer(name(RestConnectionImpl.class, "create.request")); deleteTimer = metricRegistry.timer(name(RestConnectionImpl.class, "delete.request")); describeGlobalTimer = metricRegistry.timer(name(RestConnectionImpl.class, "describeGlobal.request")); describeSObjectTimer = metricRegistry.timer(name(RestConnectionImpl.class, "describeSObject.request")); basicSObjectInfoTimer = metricRegistry.timer(name(RestConnectionImpl.class, "getBasicSObjectInfo.request")); queryTimer = metricRegistry.timer(name(RestConnectionImpl.class, "query.request")); queryMoreTimer = metricRegistry.timer(name(RestConnectionImpl.class, "queryMore.request")); retrieveTimer = metricRegistry.timer(name(RestConnectionImpl.class, "retrieve.request")); searchTimer = metricRegistry.timer(name(RestConnectionImpl.class, "search.request")); updateTimer = metricRegistry.timer(name(RestConnectionImpl.class, "update.request")); upsertTimer = metricRegistry.timer(name(RestConnectionImpl.class, "upsert.request")); } @Override @Nonnull public SaveResult create(SObject sObject) throws IOException { Timer.Context context = createTimer.time(); try { return getSaveResult(this.getHttpApiClient().create(sObject)); } finally { context.stop(); } } @Override public void delete(String sObjectType, Id id) throws IOException { Timer.Context context = deleteTimer.time(); try { this.getHttpApiClient().delete(sObjectType, id); } finally { context.stop(); } } @Override @Nonnull public DescribeGlobalResult describeGlobal() throws IOException { Timer.Context context = describeGlobalTimer.time(); String describeGlobalJson; try { describeGlobalJson = this.getHttpApiClient().describeGlobal(); } finally { context.stop(); } ObjectNode objectNode = this.objectReader.withType(ObjectNode.class).readValue(describeGlobalJson); String encoding = objectNode.get("encoding").textValue(); int maxBatchSize = objectNode.get("maxBatchSize").intValue(); ArrayNode descriptionsNode = this.objectReader.withType(ArrayNode.class) .readValue(objectNode.get("sobjects")); Iterator<JsonNode> elements = descriptionsNode.elements(); List<GlobalSObjectDescription> descriptions = Lists.newArrayList(); while (elements.hasNext()) { JsonNode node = elements.next(); descriptions.add(this.objectReader.readValue(node.traverse(), BasicSObjectMetadata.class)); } return new DescribeGlobalResult(encoding, maxBatchSize, descriptions); } @Override @Nonnull public SObjectDescription describeSObject(String sObjectType) throws IOException { Timer.Context context = describeSObjectTimer.time(); String descrJson; try { descrJson = this.getHttpApiClient().describeSObject(sObjectType); } finally { context.stop(); } return this.objectReader.withType(SObjectDescription.class).readValue(descrJson); } @Override @Nonnull public BasicSObjectMetadataResult getBasicObjectInfo(String sObjectType) throws IOException { Timer.Context context = basicSObjectInfoTimer.time(); String jsonStr; try { jsonStr = this.getHttpApiClient().basicSObjectInfo(sObjectType); } finally { context.stop(); } ObjectNode objectNode = this.objectReader.withType(ObjectNode.class).readValue(jsonStr); BasicSObjectMetadata metadata = this.objectReader.withType(BasicSObjectMetadata.class) .readValue(objectNode.get("objectDescribe")); ArrayNode recentItems = this.objectReader.withType(ArrayNode.class) .readValue(objectNode.get("recentItems")); List<SObject> sObjects = getSObjects(recentItems.elements()); return new BasicSObjectMetadataResult(metadata, sObjects); } @Override @Nonnull public RestQueryResult query(String soql) throws IOException { Timer.Context context = queryTimer.time(); String json; try { json = this.getHttpApiClient().query(soql); } finally { context.stop(); } return getQueryResult(this.objectReader.readValue(parse(json), JsonNode.class)); } @Override @Nonnull public RestQueryResult queryMore(RestQueryLocator queryLocator) throws IOException { Timer.Context context = queryMoreTimer.time(); String json; try { json = this.getHttpApiClient().queryMore(queryLocator); } finally { context.stop(); } return getQueryResult(this.objectReader.readValue(parse(json), JsonNode.class)); } @Override @Nonnull public SObject retrieve(String sObjectType, Id id, List<String> fields) throws IOException { Timer.Context context = retrieveTimer.time(); String json; try { json = this.getHttpApiClient().retrieve(sObjectType, id, fields); } finally { context.stop(); } return getSObject(this.objectReader.readValue(parse(json), JsonNode.class)); } @Override @Nonnull public List<SObject> search(String sosl) throws IOException { Timer.Context context = searchTimer.time(); String json; try { json = this.getHttpApiClient().search(sosl); } finally { context.stop(); } return getSObjects(this.objectReader.readValue(parse(json), ArrayNode.class).elements()); } @Override public void update(SObject sObject) throws IOException { Timer.Context context = updateTimer.time(); try { this.getHttpApiClient().update(sObject); } finally { context.stop(); } } @Override @Nonnull public UpsertResult upsert(SObject sObject, String externalIdField) throws IOException { // TODO write tests for upsert Timer.Context context = upsertTimer.time(); int statusCode; try { statusCode = this.getHttpApiClient().upsert(sObject, externalIdField); } finally { context.stop(); } if (statusCode == 204) { return UpsertResult.UPDATED; } return UpsertResult.CREATED; } @Nonnull private JsonParser parse(@Nullable String str) throws IOException { return objectReader.getFactory().createParser(str); } @Nonnull private HttpApiClient getHttpApiClient() { return this.httpApiClientProvider.getClient(); } /** * @param elements an iterator across json elements, each of which represents an sObject * * @return list of SObjects * * @throws IOException on error */ @Nonnull private static List<SObject> getSObjects(Iterator<JsonNode> elements) throws IOException { List<SObject> sObjects = Lists.newArrayList(); while (elements.hasNext()) { JsonNode node = elements.next(); sObjects.add(getSObject(node)); } return sObjects; } @Nonnull private SaveResult getSaveResult(@Nullable String saveResultJson) throws IOException { ObjectNode objectNode = this.objectReader.withType(ObjectNode.class).readValue(parse(saveResultJson)); String id = objectNode.get("id").textValue(); boolean success = objectNode.get("success").booleanValue(); List<ApiError> errors = this.objectReader.withType(HttpApiClient.API_ERRORS_TYPE) .readValue(objectNode.get("errors")); return new SaveResultImpl(new Id(id), success, errors); } @Nonnull private static RestSObject getSObject(JsonNode rawNode) throws IOException { ObjectNode jsonNode = asObjectNode(rawNode); ObjectNode attributes = getObjectNode(jsonNode, ATTRIBUTES_KEY); String type = getString(attributes, "type"); JsonNode idNode = jsonNode.get(ID_KEY); RestSObjectImpl sObject; if (isNull(idNode)) { sObject = RestSObjectImpl.getNew(type); } else { if (!idNode.isTextual()) { throw new ResponseParseException("Id node <" + idNode + "> wasn't textual"); } sObject = RestSObjectImpl.getNewWithId(type, new Id(idNode.textValue())); } jsonNode.remove(ID_KEY); jsonNode.remove(ATTRIBUTES_KEY); Iterator<String> fieldNames = jsonNode.fieldNames(); while (fieldNames.hasNext()) { String fieldName = fieldNames.next(); JsonNode fieldValueNode = jsonNode.get(fieldName); if (fieldValueNode.isNull()) { // null node is a value node so handle it first sObject.setField(fieldName, null); continue; } else if (fieldValueNode.isValueNode()) { sObject.setField(fieldName, fieldValueNode.asText()); continue; } else if (fieldValueNode.isObject()) { // it could either be a subquery or a sub object at this point. if (fieldValueNode.path("attributes").isObject()) { sObject.setRelationshipSubObject(fieldName, getSObject(fieldValueNode)); } else if (fieldValueNode.path("records").isArray()) { sObject.setRelationshipQueryResult(fieldName, getQueryResult(fieldValueNode)); } else { throw new ResponseParseException("Could not understand field value node: " + fieldValueNode); } continue; } throw new ResponseParseException("Unknown node type <" + fieldValueNode + ">"); } return sObject; } @Nonnull private static RestQueryResult getQueryResult(JsonNode rawNode) throws IOException { ObjectNode results = asObjectNode(rawNode); int totalSize = getInt(results, "totalSize"); boolean done = getBoolean(results, "done"); ArrayNode records = getArrayNode(results, "records"); List<RestSObject> sObjects = Lists.newArrayList(); Iterator<JsonNode> elements = records.elements(); while (elements.hasNext()) { JsonNode recordNode = elements.next(); sObjects.add(getSObject(recordNode)); } if (done) { return RestQueryResultImpl.getDone(sObjects, totalSize); } String nextRecordsUrl = getString(results, "nextRecordsUrl"); return RestQueryResultImpl.getNotDone(sObjects, totalSize, new RestQueryLocator(nextRecordsUrl)); } @Nonnull private static ObjectNode asObjectNode(JsonNode jsonNode) throws ResponseParseException { if (jsonNode == null) { throw new ResponseParseException("Got a null object node"); } if (!jsonNode.isObject()) { throw new ResponseParseException("Got a node that wasn't an object <" + jsonNode + ">"); } return (ObjectNode) jsonNode; } @Nonnull private static String getString(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode node = getNode(jsonNode, key); if (!node.isTextual()) { throw new ResponseParseException("Node <" + node + "> isn't text for key <" + key + ">"); } return node.textValue(); } private static int getInt(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode node = getNode(jsonNode, key); if (!node.isInt()) { throw new ResponseParseException("Node <" + node + "> isn't int for key <" + key + ">"); } return node.intValue(); } private static boolean getBoolean(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode node = getNode(jsonNode, key); if (!node.isBoolean()) { throw new ResponseParseException("Node <" + node + "> isn't boolean for key <" + key + ">"); } return node.booleanValue(); } @Nonnull private static ArrayNode getArrayNode(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode node = getNode(jsonNode, key); if (!node.isArray()) { throw new ResponseParseException("Node <" + node + "> isn't an array for key <" + key + ">"); } return (ArrayNode) node; } @Nonnull private static ObjectNode getObjectNode(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode node = getNode(jsonNode, key); if (!node.isObject()) { throw new ResponseParseException("Node <" + node + "> isn't an object for key <" + key + ">"); } return (ObjectNode) node; } @Nonnull private static JsonNode getNode(ObjectNode jsonNode, String key) throws ResponseParseException { JsonNode value = jsonNode.get(key); if (isNull(value)) { throw new ResponseParseException("Null value for key <" + key + ">"); } return value; } private static boolean isNull(JsonNode node) { return node == null || node == NullNode.instance; } }