[SalesForce] Tooling API in Apex – polymorphism, generic query() callout

I am working on Salesforce project heavily depending on use of Tooling API (we are executing tests on remote SF organizations and gathering results).

What bothers me is I cannot use polymporphism when calling Tooling API via SOAP.
Even though I use most of the time one SOAP method query, for every object I have to create separate method and separate schema response element, for example:

public ToolingApi.QueryResultApexCodeCoverage queryApexCodeCoverage(String queryString) {
            if (Test.isRunningTest()) {
                Test.setMock(WebServiceMock.class, new MockResponses.queryApexCodeCoverageMock());
            }
            ToolingApi.query_element request_x = new ToolingApi.query_element();
            request_x.queryString = queryString;
            ToolingApi.queryResponseApexCodeCoverage_element response_x;
            Map<String, ToolingApi.queryResponseApexCodeCoverage_element> response_map_x = new Map<String, ToolingApi.queryResponseApexCodeCoverage_element>();
            response_map_x.put('response_x', response_x);
            WebServiceCallout.invoke(
                    this,
                    request_x,
                    response_map_x,
                    new String[]{
                            endpoint_x,
                            '',
                            'urn:tooling.soap.sforce.com',
                            'query',
                            'urn:tooling.soap.sforce.com',
                            'queryResponse',
                            'ToolingApi.queryResponseApexCodeCoverage_element'
                    }
            );
            response_x = response_map_x.get('response_x');
            return response_x.result;
        }

Schema:

public class queryResponseApexTestResult_element {
        public ToolingApi.QueryResultApexTestResult result;
        ...
    }

public class QueryResultApexTestResult {
        public Boolean done;
        public String entityTypeName;
        public String nextRecordsUrl;
        public String queryLocator;
        public ToolingApi.ApexTestResult[] records;
        public Integer size;
        public Integer totalSize;
        ...
    }

It would be simpler, better and shorter code to have just one of these for every query. I tried to use different tricks here, for example using generic QueryResult with public sObject[] records and then casting it to class that is expected to be return, I also tried calling Tooling API via REST and playing a bit with JSON.deserializeUntyped()none of these worked for me. Any experience with such cases? What could be solution to simplify the code?

Best Answer

As you found, if you just use the direct QueryResult against the SOAP API and include anything in the fields beyond the ID you get the following response:

SOQL:

Select Id, Name from ApexClass

System.CalloutException: Web service callout failed: Unable to parse callout response. Apex type not found for element Name

The problem is that the underlying WebServiceCallout.invoke can't unpack the specific sObject into the generic QueryResults.records sObject as it doesn't have a generic set of fields like the Partner API does.

E.g. The Name field here is a specific extension element over ens:sObject.

    <records xsi:type="sf:ApexClass">
      <sf:Id>01p70000000br8TAAQ</sf:Id>
      <sf:Name>ApexClassNameHere</sf:Name> <!-- What to do with this? -->
    </records>

It's a pain, but you could carry on with your current approach of breaking out the specific classes that extend ens:sObject (plus the associated *_element to unpack it).

In terms of classes that extend sObject, pretty much everything you can query falls into that category. Let's look at ApexClass.

<xsd:complexType name="ApexClass">
<xsd:complexContent>
 <xsd:extension base="ens:sObject">
  <xsd:sequence>
   <xsd:element name="ApiVersion" minOccurs="0" type="xsd:double" nillable="true"/>
   <xsd:element name="Body" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="BodyCrc" minOccurs="0" type="xsd:double" nillable="true"/>
   <xsd:element name="CreatedBy" minOccurs="0" type="ens:User" nillable="true"/>
   <xsd:element name="CreatedById" minOccurs="0" type="tns:ID" nillable="true"/>
   <xsd:element name="CreatedDate" minOccurs="0" type="xsd:dateTime" nillable="true"/>
   <xsd:element name="FullName" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="IsValid" minOccurs="0" type="xsd:boolean" nillable="true"/>
   <xsd:element name="LastModifiedBy" minOccurs="0" type="ens:User" nillable="true"/>
   <xsd:element name="LastModifiedById" minOccurs="0" type="tns:ID" nillable="true"/>
   <xsd:element name="LastModifiedDate" minOccurs="0" type="xsd:dateTime" nillable="true"/>
   <xsd:element name="LengthWithoutComments" minOccurs="0" type="xsd:int" nillable="true"/>
   <xsd:element name="ManageableState" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="Metadata" minOccurs="0" type="mns:ApexClass" nillable="true"/>
   <xsd:element name="Name" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="NamespacePrefix" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="Status" minOccurs="0" type="xsd:string" nillable="true"/>
   <xsd:element name="SymbolTable" minOccurs="0" type="tns:SymbolTable" nillable="true"/>
   <xsd:element name="SystemModstamp" minOccurs="0" type="xsd:dateTime" nillable="true"/>
  </xsd:sequence>
 </xsd:extension>
</xsd:complexContent>

So to actually use that from Apex you need the the class to deseralize into. Last I checked the built in version of Wsdl2Apex won't generate these classes for you. It needs to be generated with both the fields from the parent complexType and the child fields.

public class ApexClass extends sObject_x {
    public String type = 'ApexClass';
    private String[] type_att_info = new String[]{'xsi:type'};
    public Double ApiVersion;
    public String Body;
    public Double BodyCrc;
    public ToolingAPIWSDL.User_x CreatedBy;
    public String CreatedById;
    public DateTime CreatedDate;
    public String FullName;
    public Boolean IsValid;
    public ToolingAPIWSDL.User_x LastModifiedBy;
    public String LastModifiedById;
    public DateTime LastModifiedDate;
    public Integer LengthWithoutComments;
    public ToolingAPIWSDLMetadata.ApexClass Metadata;
    public String Name;
    public String NamespacePrefix;
    public String Status;
    public ToolingAPIWSDL.SymbolTable SymbolTable;
    public DateTime SystemModstamp;
    public String[] fieldsToNull;
    public String Id;
    private String[] ApiVersion_type_info = new String[]{'ApiVersion','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] Body_type_info = new String[]{'Body','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] BodyCrc_type_info = new String[]{'BodyCrc','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] CreatedBy_type_info = new String[]{'CreatedBy','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] CreatedById_type_info = new String[]{'CreatedById','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] CreatedDate_type_info = new String[]{'CreatedDate','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] FullName_type_info = new String[]{'FullName','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] IsValid_type_info = new String[]{'IsValid','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] LastModifiedBy_type_info = new String[]{'LastModifiedBy','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] LastModifiedById_type_info = new String[]{'LastModifiedById','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] LastModifiedDate_type_info = new String[]{'LastModifiedDate','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] LengthWithoutComments_type_info = new String[]{'LengthWithoutComments','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] Metadata_type_info = new String[]{'Metadata','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] Name_type_info = new String[]{'Name','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] NamespacePrefix_type_info = new String[]{'NamespacePrefix','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] Status_type_info = new String[]{'Status','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] SymbolTable_type_info = new String[]{'SymbolTable','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] SystemModstamp_type_info = new String[]{'SystemModstamp','urn:tooling.soap.sforce.com',null,'0','1','true'};
    private String[] fieldsToNull_type_info = new String[]{'fieldsToNull','urn:tooling.soap.sforce.com',null,'0','-1','true'};
    private String[] Id_type_info = new String[]{'Id','urn:tooling.soap.sforce.com',null,'1','1','true'};
    private String[] apex_schema_type_info = new String[]{'urn:tooling.soap.sforce.com','true','false'};
    private String[] field_order_type_info = new String[]{'ApiVersion','Body','BodyCrc','CreatedBy','CreatedById','CreatedDate','FullName','IsValid','LastModifiedBy','LastModifiedById','LastModifiedDate','LengthWithoutComments','Metadata','Name','NamespacePrefix','Status','SymbolTable','SystemModstamp','fieldsToNull','Id'};
}

There are ways to automate the generation of these classes from the WSDL.


One thing you might want to investigate is the apex-toolingapi repo. It has a pretty generic query method implementation returning a QueryResult.

    public ToolingAPIWSDL.QueryResult query(String queryString) {
        ToolingAPIWSDL.query_element request_x = new ToolingAPIWSDL.query_element();
        request_x.queryString = queryString;
        ToolingAPIWSDL.queryResponse_element response_x;
        Map<String, ToolingAPIWSDL.queryResponse_element> response_map_x = new Map<String, ToolingAPIWSDL.queryResponse_element>();
        response_map_x.put('response_x', response_x);
        System.debug('JWL: request: ' + request_x);
        System.debug('JWL: this endpoint: ' + this.endpoint_x);
        WebServiceCallout.invoke(
          this,
          request_x,
          response_map_x,
          new String[]{endpoint_x,
          '',
          'urn:tooling.soap.sforce.com',
          'query',
          'urn:tooling.soap.sforce.com',
          'queryResponse',
          'ToolingAPIWSDL.queryResponse_element'}
        );
        response_x = response_map_x.get('response_x');
        return response_x.result;
    }

Yet somehow they don't run into the "Apex type not found" issues and can then generically unpack to the required class type.

public ToolingAPIWSDL.QueryResult query(String queryString,String recordClassName) {
    ToolingAPIWSDL.QueryResult qr = service.query(queryString);
    return castQueryResultToSubclass(qr,recordClassName);
}

How have they performed this miraculously feat of deserialization? Are they wizards?

Spoiler:

They cheated and put all the possible response fields on sObject_x.
Have a look at lines 178 to 417...

Related Topic