8

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?

  • BTW, Question is also applicable for metadata API. – Wookie88 Nov 16 '18 at 11:23
  • So you are having problems while querrying the test results? – Pranay Jaiswal Nov 16 '18 at 12:25
  • Not problems with querying itself. The problem is that I have to create separate query method for every object I need to query. – Wookie88 Nov 16 '18 at 13:40
4
+50

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

  • Hi, thanks for the answer. The second part is good way to eliminate need of cloning the query methods for every object type queried, however at the cost of making sObject_x class in schema something like Frankenstein's monster - my inner perfectionist is not content with such approach, but I think I may go for it. But before that could you please elaborate a bit on first part - the classes that extend ens:sObject? – Wookie88 Nov 19 '18 at 13:23
  • @Wookie88 When I find a bit more time I'll expand further on how to generate out the required child classes and then (hopefully) dynamically pass them through the query method. – Daniel Ballinger Nov 20 '18 at 1:26
1

When you mean generic, you cannot use Tooling api to query both Normal Sobjects and Tooling API objects.

Querying Account on tooling API endpoint will give an exception

[{"message":"sObject type 'Account' is not supported.","errorCode":"INVALID_TYPE"}]

and similarly querying Tooling object via normal query endpoint will give below

[{"message":"sObject type 'ApexCodeCoverage' is not supported.","errorCode":"INVALID_TYPE"}]

Ofcourse there are common object in salesforce that can be querried with Both Tooling and Rest Querry Endpoint,but they are few (ApexClass,ApexTrigger)

The error you mentioned while using deserializeUntyped to Map happened to me as well when I tried to use tooling endpoint to querry Account. Which is fine as you can see from above response that will return List and not Map.

That being said, I wanted to test this deeply so I created a utility class that will work for both Tooling and non-tooling objects.

 public class GenericeObjectQuerryService {

        public static void querryRecords(String querry,boolean isTooling){

            String endpoint;
            if(isTooling){
                endpoint =Url.getOrgDomainUrl().toExternalForm()+ '/services/data/v43.0/tooling/query?q='+EncodingUtil.urlEncode(querry,'UTF-8');
            }else{
                endpoint =Url.getOrgDomainUrl().toExternalForm()+ '/services/data/v43.0/query?q='+EncodingUtil.urlEncode(querry,'UTF-8');

            }

            String responseString = GenericeObjectQuerryService.doCallout(endpoint,'GET');
            Map<String,Object> responseMap = (Map<String,Object>)JSON.deserializeUntyped(responseString);
            System.debug(responseMap);
            // This is Deserialized without giving any exception


        }

        private static String doCallout(String endpoint,String methodType){  
            HttpRequest hr = new HttpRequest();
            hr.setEndpoint(endpoint);
            hr.setMethod(methodType);
            hr.setHeader('Authorization', 'Bearer ' + UserInfo.getSessionId());
            HttpResponse hresp = new Http().send(hr);
            return hresp.getBody();


        }

    }

Now here comes the test part.

I Querry Account using normal rest

GenericeObjectQuerryService.querryRecords('Select Id from Account LIMIT 1',false); //Tooling false, using sobject query endpoint

//Runs no exception . Debugs account record

Now enabling tooling on account:

GenericeObjectQuerryService.querryRecords('Select Id from Account LIMIT 1',true); 

Debug :

enter image description here

So to answer, Yes its is possible to create generic query method, given that you know if the Object you are using in query should be querried on normal endpoint or tooling endpoint.

  • Sorry, this does not answer my question. By generic I meant every Tooling API-specific object. I am not trying to query for sObjects using Tooling API. – Wookie88 Nov 19 '18 at 13:14
  • Can you mention what desrialize error you are getting when querring via Rest Endpoint? @Wookie88 because I am not – Pranay Jaiswal Nov 19 '18 at 13:22
  • Unfortunately, I removed my code for REST API, I decided to stay with SOAP. The ultimate goal is to be able to every query Tooling API using one method that accepts the query as parameter and then of course retrieve all the fields values while keeping the code as short and elegant as possible of course ;-). – Wookie88 Nov 19 '18 at 13:30
  • In anycase if u wanna use Rest API, you can refer the above. Cheerss – Pranay Jaiswal Nov 19 '18 at 13:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.