/* * 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.soap; import com.palominolabs.crm.sf.core.Id; import com.palominolabs.crm.sf.core.SObject; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Contains convenience methods for manipulating SObjects. */ @Immutable final class SObjects { /** * used to create Document objects */ private static final EmptyDocumentFactory DOC_FACTORY = new EmptyDocumentFactory(); private SObjects() { } /** * Convert an individual SObject stub into a facade SObject. * * @param stubSObject the stub sobject to convert * * @return SObject, or null if the input was null * * @throws SObjectConversionException if conversion fails */ @Nullable private static PartnerSObject convertStubSObjectToFacadeSObject( @Nullable com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject stubSObject) throws SObjectConversionException { if (stubSObject == null) { return null; } PartnerSObjectImpl newSObject; // if the sobject was queried with Id, create the facade sobject with an id if (stubSObject.getId() == null) { newSObject = PartnerSObjectImpl.getNew(stubSObject.getType()); } else { newSObject = PartnerSObjectImpl.getNewWithId(stubSObject.getType(), new Id(stubSObject.getId())); } List<Object> fields = stubSObject.getAny(); // see http://wiki.apexdevnet.com/index.php/PartnerQuery /* For dot-syntax relationship queries: <queryResponse> <result xsi:type="QueryResult"> <done>true</done> <queryLocator xsi:nil="true"/> <records xsi:type="sf:sObject"> <sf:type>Account</sf:type> <sf:Id xsi:nil="true"/> <sf:Owner xsi:type="sf:sObject"> <sf:type>User</sf:type> <sf:Id xsi:nil="true"/> <sf:Name>sftestorg3 mpierce</sf:Name> </sf:Owner> </records> <size>1</size> </result> </queryResponse> */ for (Object fieldObj : fields) { Element xmlElt = (Element) fieldObj; String xsiTypeValue = xmlElt.getAttribute("xsi:type"); String fieldName = xmlElt.getLocalName(); if ("QueryResult".equals(xsiTypeValue)) { PartnerQueryResult subqueryResult = parseQueryResult(xmlElt); newSObject.setRelationshipQueryResult(fieldName, subqueryResult); } else if ("sf:sObject".equals(xsiTypeValue)) { PartnerSObject subObj = parseSObject(xmlElt, fieldName); newSObject.setRelationshipSubObject(fieldName, subObj); } else { String fieldValue = extractFieldValue(xmlElt); newSObject.setField(fieldName, fieldValue); } } return newSObject; } /** * @param fieldNode the xml element for an individual field in the <any> part of an SObject * * @return the string value, or null if no text node was found */ @Nullable private static String extractFieldValue(@Nonnull Node fieldNode) { // this is the text node inside the element, or null if no such child. // This (coincidentally) is the case when a field is sent over with xsi:nil="true". // TODO really check for xsi:nil Node firstChild = fieldNode.getFirstChild(); if (firstChild != null) { // for text nodes, this is the content of the node return firstChild.getNodeValue(); } return null; } /** * @param qrElement the dom node that is the root of the query result * * @return a QueryResult * * @throws SObjectConversionException if the data cannot be extracted from the xml */ @Nonnull private static PartnerQueryResult parseQueryResult(@Nonnull Element qrElement) throws SObjectConversionException { /* QR structure: <complexType name="QueryResult"> <sequence> <element name="done" type="xsd:boolean"/> <element name="queryLocator" type="tns:QueryLocator" nillable="true"/> <element name="records" type="ens:sObject" nillable="true" minOccurs="0" maxOccurs="unbounded"/> <element name="size" type="xsd:int"/> </sequence> </complexType> <simpleType name="QueryLocator"> <restriction base="xsd:string"/> </simpleType> */ /* For subqueries: <queryResponse> <result xsi:type="QueryResult"> <done>true</done> <queryLocator xsi:nil="true"/> <records xsi:type="sf:sObject"> <sf:type>Account</sf:type> <sf:Id>0015000000WWD7bAAH</sf:Id> <sf:Id>0015000000WWD7bAAH</sf:Id> <sf:Name>United Oil & Gas, Singapore</sf:Name> <sf:AnnualRevenue xsi:nil="true"/> <sf:Contacts xsi:type="QueryResult"> <!-- start of sub query result --> <done>true</done> <queryLocator xsi:nil="true"/> <records xsi:type="sf:sObject"> <sf:type>Contact</sf:type> <sf:Id>0035000000km1owAAA</sf:Id> <sf:Id>0035000000km1owAAA</sf:Id> <sf:FirstName>Liz</sf:FirstName> <sf:Email>ldcruz@uog.com</sf:Email> </records> <records xsi:type="sf:sObject"> <sf:type>Contact</sf:type> <sf:Id>0035000000km1ovAAA</sf:Id> <sf:Id>0035000000km1ovAAA</sf:Id> <sf:FirstName>Tom</sf:FirstName> <sf:Email>tripley@uog.com</sf:Email> </records> <size>2</size> </sf:Contacts> <!-- end of sub query result --> <sf:Tasks xsi:nil="true"/> <!-- empty sub query result --> <sf:Cases xsi:type="QueryResult"> <done>true</done> <queryLocator xsi:nil="true"/> <records xsi:type="sf:sObject"> <sf:type>Case</sf:type> <sf:Id>5005000000AaQxoAAF</sf:Id> <sf:Id>5005000000AaQxoAAF</sf:Id> <sf:Subject>Maintenance guidelines for generator unclear</sf:Subject> </records> <records xsi:type="sf:sObject"> <sf:type>Case</sf:type> <sf:Id>5005000000AaQxtAAF</sf:Id> <sf:Id>5005000000AaQxtAAF</sf:Id> <sf:Subject>Frequent mechanical breakdown</sf:Subject> </records> <records xsi:type="sf:sObject"> <sf:type>Case</sf:type> <sf:Id>5005000000AaQxpAAF</sf:Id> <sf:Id>5005000000AaQxpAAF</sf:Id> <sf:Subject>Electronic panel fitting loose</sf:Subject> </records> <size>3</size> </sf:Cases> </records> <!-- bottom of one sobject that contained subqueries --> <size>1</size> </result> </queryResponse> */ NodeList childNodes = qrElement.getChildNodes(); if (childNodes.getLength() < 3) { throw new SObjectConversionException("Query result element had only " + childNodes.getLength() + " nodes"); } Node doneNode = childNodes.item(0); checkNodeIsElement(doneNode, "done"); boolean isDone = Boolean.parseBoolean(doneNode.getTextContent()); List<PartnerSObject> sObjects = new ArrayList<PartnerSObject>(); // the last node is "size", not an SObject for (int i = 2; i < childNodes.getLength() - 1; i++) { sObjects.add(parseSObject(childNodes.item(i), "records")); } Node sizeNode = childNodes.item(childNodes.getLength() - 1); Element sizeElt = checkNodeIsElement(sizeNode, "size"); int sizeInt = Integer.parseInt(sizeElt.getTextContent()); if (isDone) { return PartnerQueryResultImpl.getDone(sObjects, sizeInt); } // not done, so extract the locator Node queryLocNode = childNodes.item(1); Element queryLocElement = checkNodeIsElement(queryLocNode, "queryLocator"); String locXsiNil = queryLocElement.getAttribute("xsi:nil"); if ("true".equals(locXsiNil)) { throw new SObjectConversionException("Got a nil locator with a not-done query result"); } // if xsi:nil is something other than true, it must be a real locator String locatorString = queryLocElement.getTextContent(); return PartnerQueryResultImpl.getNotDone(sObjects, sizeInt, new PartnerQueryLocator(locatorString)); } /** * Check that a node is non-null, is an element, and has the expected local name. If all checks pass, the node is * cast to an element and returned. * * @param node the node to check * @param desiredElementName the element name that we're expecting the node to be * * @return the node as an element * * @throws SObjectConversionException if the node is null or the node has the wrong local name */ private static Element checkNodeIsElement(@Nullable Node node, @Nonnull String desiredElementName) throws SObjectConversionException { if (node == null) { throw new SObjectConversionException("No node found, expecting " + desiredElementName); } if (node.getNodeType() != Node.ELEMENT_NODE) { throw new SObjectConversionException("Node is not an element, type is " + node.getNodeType()); } if (!node.getLocalName().equals(desiredElementName)) { throw new SObjectConversionException( "node was <" + node.getLocalName() + "> instead of " + desiredElementName); } return (Element) node; } private static PartnerSObject parseSObject(@Nonnull Node sObjectNode, String expectedNodeLocalName) throws SObjectConversionException { Element sObjElt = checkNodeIsElement(sObjectNode, expectedNodeLocalName); String parentNodeTypeStr = sObjElt.getAttribute("xsi:type"); if (!"sf:sObject".equals(parentNodeTypeStr)) { throw new SObjectConversionException( "Type of the sobject node is <" + parentNodeTypeStr + "> instead of xsi:type"); } /* SObject schema <complexType name="sObject"> <sequence> <element name="type" type="xsd:string"/> <element name="fieldsToNull" type="xsd:string" nillable="true" minOccurs="0" maxOccurs="unbounded"/> <element name="Id" type="tns:ID" nillable="true"/> <any namespace="##targetNamespace" minOccurs="0" maxOccurs="unbounded" processContents="lax"/> </sequence> </complexType> */ /* Sample: <records xsi:type="sf:sObject"> <sf:type>Contact</sf:type> <sf:Id xsi:nil="true"/> <sf:FirstName>Stella</sf:FirstName> <sf:LastName>Pavlova</sf:LastName> </records> */ NodeList sObjChildNodes = sObjElt.getChildNodes(); Node typeNode = sObjChildNodes.item(0); checkNodeIsElement(typeNode, "type"); String sObjTypeStr = typeNode.getTextContent(); Node fieldsToNullOrId = sObjChildNodes.item(1); Element idElt = checkNodeIsElement(fieldsToNullOrId, "Id"); PartnerSObject sObj; if ("true".equals(idElt.getAttribute("xsi:nil"))) { // id is null sObj = PartnerSObjectImpl.getNew(sObjTypeStr); } else { Id id = new Id(idElt.getTextContent()); sObj = PartnerSObjectImpl.getNewWithId(sObjTypeStr, id); } for (int i = 2; i < sObjChildNodes.getLength(); i++) { Node fieldNode = sObjChildNodes.item(i); String fieldName = fieldNode.getLocalName(); String fieldValue = extractFieldValue(fieldNode); sObj.setField(fieldName, fieldValue); } return sObj; } /** * Convert an instance of our facade sobject into a corresponding stub objecta * * @param facadeSObject the sobject to convert * * @return a stub sobject * * @throws SObjectConversionException if xml wrangling fails */ @Nonnull static com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject convertFacadeSObjectToStubSObject( @Nonnull SObject facadeSObject) throws SObjectConversionException { com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject stub = new com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject(); if (facadeSObject.getId() == null) { stub.setId(null); } else { stub.setId(facadeSObject.getId().toString()); } stub.setType(facadeSObject.getType()); List<Object> stubFields = stub.getAny(); Document doc = DOC_FACTORY.newDocument(); // create an Element for each field containing a Node that has the value Iterator<String> fieldNameIter = facadeSObject.getAllFields().keySet().iterator(); Element fieldElt; Node valueNode; String fieldName; String value; while (fieldNameIter.hasNext()) { fieldName = fieldNameIter.next(); value = facadeSObject.getField(fieldName); if (value == null) { stub.getFieldsToNull().add(fieldName); } else { try { fieldElt = doc.createElement(fieldName); valueNode = doc.createTextNode(value); fieldElt.appendChild(valueNode); } catch (DOMException e) { throw new SObjectConversionException( "Couldn't create DOM nodes for field name <" + fieldName + "> and value <" + value + ">", e); } stubFields.add(fieldElt); } } return stub; } /** * Convert a whole list of soap stub SObjects into facade SObjects * * @param stubSObjects list of stub sobjects * * @return list of facade SObjects * * @throws SObjectConversionException if the facade sobjects can't be extracted */ @SuppressWarnings("TypeMayBeWeakened") static List<PartnerSObject> convertStubListToSObjectList( List<com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject> stubSObjects) throws SObjectConversionException { List<PartnerSObject> sObjects = new ArrayList<PartnerSObject>(); for (com.palominolabs.crm.sf.soap.jaxwsstub.partner.SObject stub : stubSObjects) { sObjects.add(convertStubSObjectToFacadeSObject(stub)); } return sObjects; } /** * Creating a DocumentBuilderFactory and DocumentBuilder is 2000x slower (really! I benchmarked!) than creating a * Document from an existing DocumentBuilder. Thus, we create only one factory and builder by using an instance of * this class as a static field in the parent class. */ @ThreadSafe private static final class EmptyDocumentFactory { /** * The doc builder is not necessarily thread safe, so access to it must be synchronized. */ private final DocumentBuilder docBuilder; /** * Non-private for performance. */ EmptyDocumentFactory() { DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); try { this.docBuilder = docBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new IllegalStateException("Somehow the doc builder factory couldn't create a doc builder", e); } } /** * Non-private for performance. * * @return a new Document object */ synchronized Document newDocument() { return this.docBuilder.newDocument(); } } }