Salesforce.com Partner SOAP API JAX-WS Tutorial Part 3

This is Part 3 in a series. Read Part 1, Part 2, and Part 4 to get up to speed. Update: I’ve released an open source library that presents a better interface to the Partner API (as well as other Salesforce APIs).

Making a query()

There are several API calls you can use to get data from an organization, but query() is probably the easiest one to understand because it acts the most like using SQL with a RDBMS like MySQL or PostgreSQL. Instead of SQL, though, you use a SQL-like language called SOQL (Salesforce Object Query Language). You can read about what SOQL can do in its documentation, but for now we’ll stick with a basic query: SELECT Id, FirstName, LastName, MyCustomField__c FROM Contact WHERE Email LIKE '%@gmail.com'

If you’ve used SQL, you’re probably thinking this looks like you’re selecting some columns from the Contact table and filtering on email addresses that end with ‘@gmail.com’. Fortunately, you’d be correct (though in SOQL you would say you’re selecting some object fields instead of columns).

To actually execute a query, you’ll first need to create the parameter object for the query, just like you needed to create a login parameter object for the login() call, and then set your query string (the SOQL query) in the parameter.

Query queryParam = new Query();
queryParam.setQueryString(queryStr);

Once you have the query parameter object populated with your query string, you can pass it to the query() method. I’m not doing any exception handling in the sample code, but you should certainly do so in your own code.

QueryResponse qResponse = port.query(queryParam);
QueryResultType result = qResponse.getResult();

Now you have a QueryResult. The query result has four pieces of information: a query locator, a ‘done’ boolean, an array of sObjects, and a ‘size’ int. Query results can have at most 2000 result sObjects, so the query locator is an identifier to allow you to continue getting results for a query that matched over 2000 objects. The ‘done’ boolean will be false if you need to use the query locator with the queryMore() call to continue getting results. That’s pretty straightforward, so we’ll continue on to the sObject array and the size int instead of getting into queryMore(). The size of the result is how many objects are in this result object (in other words, the size is at most 2000, even if the query matched other objects that need to be fetched with queryMore()). The sObject array (which is represented as a List<SObject> in the generated code) is pretty self explanatory: it’s a standard list of SObject objects. The class generated to represent the WSDL’s “sObject” element has a capital S, so when I use ‘sObject’ I’m referring to the conceptual Salesforce object, and when I use ‘SObject‘ I’m referring to the class in the JAX-WS generated code.

Extracting SObject data

Each SObject has a type, an id, a list of field names to null, and field data. The ‘field names to null’ list only applies to updating records, so we’ll ignore that for now. In the case of our example query string, the object we’re querying is Contact, so the type of each returned SObject would be the string “Contact”. The Id string of each SObject can be null if you don’t include Id in the list of fields to query, but in this case it is non null since we included Id in the list. The field data is more problematic. If you look in the WSDL, you’ll see the following:

<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>

The problem is the <any> element. Since the WSDL specifies a sequence of ‘any’ data, the best that JAX-WS can do when decoding this is to provide it as a List<Object>. We can use a sample query XML to figure out what is used to represent the ‘any’. In the sample query data, we can see that the ‘any’ corresponds to a list of elements where the element name is the field name and the element body is the value of the field. (Note that the Id element appears twice in the raw XML — once for the specific Id element declared in the WSDL and once because the ‘any’ contains all fetched fields. This is legal given the schema specified in the WSDL, though JAX-WS appears to ignore the second Id element. This has the consequence of making ‘Id’ not appear in the field data, so it is only accessible via the getId() method on SObject.)

The sample query data gives us the hint we need to figure out how to access field names and values. They’re simply XML elements, so we can cast them to org.w3c.dom.Element objects (which you can confirm in the debugger, if you choose). So, you can extract field names and values like this:

for (Object fieldObj :  stubSObject.getAny()) {
    Element xmlField = (Element) fieldObj;
 
    String fieldName = xmlField.getLocalName();
    String fieldValue = null;
    Node firstChild = xmlField.getFirstChild();
 
    if (firstChild != null) {
        fieldValue = firstChild.getNodeValue();
    }
 
    // do something with fieldName and fieldValue
}

Using this technique, you can extract the data from sObjects returned by other API calls that return sObjects (retrieve(), queryMore(), etc) as well.

In Part 4 of this series, I’ll talk about how to upload data into a Salesforce.com organization.

  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Twitter
  • Google Bookmarks
  • DZone
  • HackerNews
  • LinkedIn
  • Reddit
  • James Kingsbery

    First off, great tutorial.

    Unfortunately, I’m hitting some snag, and I was wondering if anyone else ran into something similar. When trying to make a query, I get the following error message:

    Exception in thread “main” javax.xml.ws.WebServiceException: com.sun.istack.XMLStreamException2: javax.xml.bind.MarshalException
    – with linked exception:
    [com.sun.istack.SAXException2: unable to marshal type “java.lang.String” as an element because it is missing an @XmlRootElement annotation]
    at com.sun.xml.ws.encoding.StreamSOAPCodec.encode(StreamSOAPCodec.java:111)
    at com.sun.xml.ws.encoding.SOAPBindingCodec.encode(SOAPBindingCodec.java:258)
    at com.sun.xml.ws.transport.http.client.HttpTransportPipe.process(HttpTransportPipe.java:153)
    at com.sun.xml.ws.transport.http.client.HttpTransportPipe.processRequest(HttpTransportPipe.java:93)
    at com.sun.xml.ws.api.pipe.Fiber.__doRun(Fiber.java:598)
    at com.sun.xml.ws.api.pipe.Fiber._doRun(Fiber.java:557)
    at com.sun.xml.ws.api.pipe.Fiber.doRun(Fiber.java:542)
    at com.sun.xml.ws.api.pipe.Fiber.runSync(Fiber.java:439)
    at com.sun.xml.ws.client.Stub.process(Stub.java:222)
    at com.sun.xml.ws.client.sei.SEIStub.doProcess(SEIStub.java:135)
    at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:109)
    at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:89)
    at com.sun.xml.ws.client.sei.SEIStub.invoke(SEIStub.java:118)
    at $Proxy35.query(Unknown Source)
    at com.worldevolved.sf.SalesForceConnector.main(SalesForceConnector.java:109)
    Caused by: com.sun.istack.XMLStreamException2: javax.xml.bind.MarshalException
    – with linked exception:
    [com.sun.istack.SAXException2: unable to marshal type “java.lang.String” as an element because it is missing an @XmlRootElement annotation]
    at com.sun.xml.ws.message.jaxb.JAXBHeader.writeTo(JAXBHeader.java:190)
    at com.sun.xml.ws.message.AbstractMessageImpl.writeTo(AbstractMessageImpl.java:135)
    at com.sun.xml.ws.encoding.StreamSOAPCodec.encode(StreamSOAPCodec.java:108)
    … 14 more
    Caused by: javax.xml.bind.MarshalException
    – with linked exception:
    [com.sun.istack.SAXException2: unable to marshal type “java.lang.String” as an element because it is missing an @XmlRootElement annotation]
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:331)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:171)
    at com.sun.xml.ws.message.jaxb.MarshallerBridge.marshal(MarshallerBridge.java:80)
    at com.sun.xml.bind.api.Bridge.marshal(Bridge.java:141)
    at com.sun.xml.bind.api.Bridge.marshal(Bridge.java:133)
    at com.sun.xml.ws.message.jaxb.JAXBHeader.writeTo(JAXBHeader.java:185)
    … 16 more
    Caused by: com.sun.istack.SAXException2: unable to marshal type “java.lang.String” as an element because it is missing an @XmlRootElement annotation
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:244)
    at com.sun.xml.bind.v2.runtime.LeafBeanInfoImpl.serializeRoot(LeafBeanInfoImpl.java:137)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:490)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:328)
    … 21 more

    I’m using JDK 6, JAXWS RI 2.1.4. I googled around, and found a few people who’ve seen similar messages but in a different context.

    • James Kingsbery

      Sorry, found my mistake. In case anyone else runs into the same kind of problem, you have to remember to wrap the session id you get back from logging in with a SessionHeader object.

      • Marshall Pierce

        Thanks for catching this. I’ve updated the relevant part of the tutorial (in Part 2) to be more explicit about handling the session header.

  • Marshall Pierce

    I haven’t seen that exception before. My first suggestion would be to try the latest version of JAX-WS RI, which is 2.1.7. Also, is this with the Partner API? Oh, and make sure you’re using the latest JDK 6 update. The version of JAXB that gets bundled with the JDK has had a fix or two sneak in as part of update 13 or so.

  • Manish

    Excellent stuff! Very well written, very comprehensive. This stuff should be on the salesforce website.

    Thank you.

  • Jaskirat

    Great, I was stuck on how to get fields value. And your article helped.

    Thanks!!

  • Pingback: Salesforce.com Partner SOAP API JAX-WS Tutorial Part 2 | Team Lazer Beez Blog

  • Arpan Chauhan

    i have sObject inside sObject.
    How to extract data in that case?

  • Arpan Chauhan

    SELECT Venue__r.name,CaseNumber,Type,Subject,Status FROM Case
    how to get Venue__r.name data from SObject ?