Thursday, November 27, 2014

Adding Eval() support to Apex

In the Codefriar blog post EVAL() in Apex Kevin presents a technique to to allow programmatic evaluation of an Apex string and the extraction or the result via an Exceptions message. Here I present an alternative approach using anonymous Apex and the debug log in place of the intentional exception.

Reasons you may or may not want to eval() Apex

Benefits

  • Where a rollback may be required the separate context via the callout doesn't require the invoking Apex to rollback. You can still progress and make further callouts and subsequent DML operations.
  • JSON can be used in the response message to return structured data.
  • An odd way of increasing limits. E.g. Each anonymous apex context gets a new set of limits.
  • You can handle classes of exception that would otherwise be uncatchable, such as System.LimitException and System.ProcedureException

Disadvantages

  • The potential for security issues depending on how the anonymous Apex is composed.
  • The requirement for users to have sufficient permissions to call executeAnonymous. Typically this means having “Author Apex” or running with the restricted access as per Executing Anonymous Apex through the API and the “Author Apex” Permission.
  • The need to parse the DEBUG log message out of the response to get the result. Other code may also write DEBUG ERROR messages, which will interfere with parsing the response. This could be addressed by improving the parsing of the Apex log. I.e. Extract all the USER_DEBUG entries to a list and then read the last one. Another alternative is to use delimiters in the debug message to make it easier to parse out.
  • Each eval() call is a callout back to the Salesforce web services. This creates limits on the number of evals that can be made. (Don't forget to add the Remote Site Setting to allow the callout)

API Background

Both the Tooling API and the older Apex API provide an executeAnonymous web method. The main difference is that the older Apex API will return the resulting Apex debug log in a SOAP header. With the tooling API the Apex debug log is generated but needs to be queried separately from the ApexLog. The older Apex API becomes more attractive here as one API call can execute the dynamic Apex code and return the log that can contains the output of the call.

By setting the DebuggingHeader correctly the size of the Apex debug log can be kept to a minimum. For example, getting on the ERROR level Apex_code messages makes extracting the required USER_DEBUG output easier and reduces the amount of superfluous data returned.

It should be noted that using executeAnonymous won't execute in the same context the way a Javascript eval() does. Any inputs need to be explicitly included in the Apex string to execute. Also, any return values need to be returned via the log and then parsed out to bring them into the context of the calling Apex.

Note that the current native Salesforce version of WSDL2Apex won't read the responses DebuggingInfo soap header. Instead these need to be read via an HttpRequest and parsing the resulting XML response.

Sample Raw SOAP Request/Response

This is sent to the Apex API at https://na5.salesforce.com/services/Soap/s/31.0

Request

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apex="http://soap.sforce.com/2006/08/apex">
   <soapenv:Header>
      <apex:DebuggingHeader>
         <apex:categories>
            <apex:category>Apex_code</apex:category>
            <apex:level>ERROR</apex:level>
         </apex:categories>
         <apex:debugLevel>NONE</apex:debugLevel>
      </apex:DebuggingHeader>
      <apex:SessionHeader>
         <apex:sessionId>00D700000000001!AQoAQGrYU000NotARealSessionIdUseYourOwnswh4QHmaPFm2fRDgk1zuXcVvWTfB4L9n7BJf</apex:sessionId>
      </apex:SessionHeader>
   </soapenv:Header>
   <soapenv:Body>
      <apex:executeAnonymous>
         <apex:String>Integer i = 314159; System.debug(LoggingLevel.Error, i);</apex:String>
      </apex:executeAnonymous>
   </soapenv:Body>
</soapenv:Envelope>

Response

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns="http://soap.sforce.com/2006/08/apex" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <soapenv:Header>
      <DebuggingInfo>
         <debugLog>31.0 APEX_CODE,ERROR
Execute Anonymous: Integer i = 314159; System.debug(LoggingLevel.Error, i);
13:24:24.027 (27564504)|EXECUTION_STARTED
13:24:24.027 (27573409)|CODE_UNIT_STARTED|[EXTERNAL]|execute_anonymous_apex
13:24:24.028 (28065096)|USER_DEBUG|[1]|ERROR|314159
13:24:24.028 (28098385)|CODE_UNIT_FINISHED|execute_anonymous_apex
13:24:24.029 (29024086)|EXECUTION_FINISHED</debugLog>
      </DebuggingInfo>
   </soapenv:Header>
   <soapenv:Body>
      <executeAnonymousResponse>
         <result>
            <column>-1</column>
            <compileProblem xsi:nil="true"/>
            <compiled>true</compiled>
            <exceptionMessage xsi:nil="true"/>
            <exceptionStackTrace xsi:nil="true"/>
            <line>-1</line>
            <success>true</success>
         </result>
      </executeAnonymousResponse>
   </soapenv:Body>
</soapenv:Envelope>

You can see the required output in the response next to |USER_DEBUG|[1]|ERROR|.

Given this the basic process becomes:

  • Build up the anonymous Apex string including any required inputs. Use a System.debug(LoggingLevel.Error, 'output here'); to send back the output data.
  • Call the Apex API executeAnonymous web method and capture the DebuggingInfo soap header in the response
  • Parse the USER_DEBUG Error message out of the Apex Log.
  • Convert the resulting string to the target data type if required.

Examples

Here are several examples showing parsing various data types from the Apex log.

 string output = soapSforceCom200608Apex.evalString('string first = \'foo\'; string second = \'bar\'; string result = first + second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals('foobar', output);
 System.debug(output);
 
 integer output = soapSforceCom200608Apex.evalInteger('integer first = 1; integer second = 5; integer result = first + second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals(6, output);
 System.debug(output);
 
 boolean output = soapSforceCom200608Apex.evalBoolean('boolean first = true; boolean second = false; boolean result = first || second; System.debug(LoggingLevel.Error, result);');
 System.assertEquals(true, output);
 System.debug(output);

 string outputJson = soapSforceCom200608Apex.evalString('List<object> result = new List<object>(); result.add(\'foo\'); result.add(12345); System.debug(LoggingLevel.Error, JSON.serialize(result));');
 System.debug(outputJson);
 List<Object> result = 
    (List<Object>)JSON.deserializeUntyped(outputJson);
 System.assertEquals(2, result.size());
 System.assertEquals('foo', result[0]);
 System.assertEquals(12345, result[1]);

Modified Apex API wrapper class

This wrapper around the Apex API SOAP web service uses HTTP Requests so that the DebuggingHeader can be extracted from the response. I've added several methods to execute the eval requests and parse out the expected response type.

Fun Addendum

Question: What happens if you have you Anonymous Apex do a recursive eval against the Anonymous API again?

E.g.:

string evalScript = 'string output = soapSforceCom200608Apex.evalString(\'string input = \\\'foo\\\'; System.debug(LoggingLevel.Error, input);\');';
System.debug(evalScript);

string output = soapSforceCom200608Apex.evalString(evalScript);
System.debug(output);

Answer:
You get a ystem.CalloutException: Callout loop not allowed response.

There is a Sfdc-Stack-Depth header that prevents the recursive calls. See Callout loop not allowed error


See also:

Friday, November 7, 2014

Deep linking to the Salesforce Developer console

One of the nice things about Salesforce initially was, given a record ID, the ability to easily build a URL to view or edit that record. E.g. you could easily view an Apex Class with:

https://na17.salesforce.com/01po0000002oeDP

Or edit the same class by appending a /e. E.g.

https://na17.salesforce.com/01po0000002oeDP/e

This is still possible, but more and more of the built in Apex editor functionality is moving into the Developer Console. Previously there was no way to deep link directly to a record in the developer console. Instead you needed to open the record using the provided menus.

With the Winter 15 release I noticed there were deep links to the Lightning Component Bundles under Setup > Build > Develop > Lightning Components

This opens the developer console with the URL:

https://na17.salesforce.com/_ui/common/apex/debug/ApexCSIPage?action=openFile&extent=AuraDefinition&Id=0Ado000000000fxCAA&AuraDefinitionBundleId=0Abo000000000Y3CAI&DefType=COMPONENT&Format=XML

This seems to work equally well with an Apex Class ID and a hand crafted URL. E.g.

https://na17.salesforce.com/_ui/common/apex/debug/ApexCSIPage?action=openFile&extent=ApexClass&Id=01po0000002oeDP

Of course, this link structure isn't officially supported. Salesforce may change it at any point in the future (as confirmed in the Dreamforce 2014 Meet the Developers session). Still, there would be times where a link directly into the Apex class or trigger in the developer console would be convenient.

IdeaExchange: Provide a deep link API to the Developer Console

Tuesday, November 4, 2014

Salesforce integration failures due to disabled SSL 3.0 (POODLE)

In response to the published security vulnerability on SSL 3.0 encryption (POODLE) Salesforce has put out the Knowledge Article Salesforce disabling SSL 3.0 encryption.

One important section with regards to web services that are called by Salesforce: (my emphasis)

Additionally, Salesforce recommends customers disable SSL 3.0 encryption in their own IT environment as soon as possible, unless they use call-out integrations. If a customer uses call-out integrations, and they have not already disabled SSL 3.0 in their own environment, then Salesforce recommends that they wait until after Salesforce has disabled SSL 3.0 for outbound requests.

The current timeline for outbound requests dropping support for SSL 3.0 is the 3rd of December 2014 for sandboxes and the 10th of December 2014 for production orgs.

If you work with a web service that disables SSL 3.0 before then you can start seeing exceptions like:

System.CalloutException
Message:  IO Exception: Remote host closed connection during handshake

I've also seen it reported manifesting in Batch Apex as:

Received fatal alert: handshake_failure

This can occur even if the web service supports TLS1.0 or higher.

Quotes attributed to support:

"We will use TLS with callouts but if it fails. We drop down to SSL and hard code to send it via SSLv3 for 24 hours or an app restart. Which ever comes first. This should address any changes that occur with the way other companies integrate with Salesforce until we completely disable SSL 3.0 on December 10th." Source
"I am from Salesforce Developer Support Team. I have taken ownership of your case regarding POODLE vulnerability. At present some outbound calls are initiated using SSLv3 ClientHello, so if this is disabled on your server, there'll be a handshake failure. Until then, it is advised that you support this for incoming calls (received from Salesforce). At present R&D and our Tech Ops organization are aggressively working on a strategy around this. Once this is finalized, there will be a tech comm broadcast as expected."
"The R&D team shall be releasing this on 11/4/2014. After that you may turn off SSLv3 without running into the handshake failures"
Source

This presents a bit of an immediate problem. Salesforce is trying to fallback to SSL 3.0 on services that only support versions of TLS. At this stage, if you don't have control of the web service or the ability to get it to accept SSL 3.0 again until mid December the only option might be to wrap it in a proxy service that does support SSL 3.0 encryption.

Another fun part of the change is that Inbound SSL 3.0 support will start phasing out from the 7th of November. Between then and the 10th of December there will be cases where Salesforce servers won't be able to call Salesforce hosted web services. At least some of the time anyway when they aren't successfully using TLS.


Update 6/11/2014: Indications are that Salesforce have been silently updating servers to prevent the fallback to SSLv3 if the target server doesn't support it. It's hard to confirm what is going on as there isn't an official known issue that can be linked to.


Update 7/11/2014: Salesforce support responded to some of the outstanding issues:

All I see you guys have various questions and I think I can answer quite a few of them, I manage part of the technical support security team at salesforce. Email me at bestebez@salesforce.com (you can look at my linkedin profile if you question who i am).

Questions that have been raised around Salesforce’s support of SSL 3.0 and TLS 1.0. While we are in the process of disabling SSL 3.0, Salesforce currently supports TLS 1.0 and TLS 1.2 for inbound requests and TLS 1.0 for outbound call-outs.
Our Technology Team has been actively working to address an issue that causes outbound call-outs to use SSL 3.0 more frequently than they should, given we have TLS 1.0 enabled. We understand that this may have caused issues for customers who have already disabled SSL 3.0 in their call-out endpoints. We released a fix to Sandboxes last Friday, October 31, and plan to release the fix to production instances during off-peak hours on Wednesday, November 5, 2014.
Many customers and partners who have tested this fix in their Sandboxes have reported successful connections using TLS 1.0. A few customers continued to experience TLS 1.0 issues on their Sandboxes, and our Technology team is working with them to find a solution.

There was an issue specifically to Na14 that was generating more outbound messages that were using SSLv3 but that has since been fixed. That is probably why a few of you guys saw an issue with.
Source

See Also:

Monday, November 3, 2014

Dreamforce 2014 Presentation - Improved Apex support for SOAP based web services

At Dreamforce this year I gave a breakout presentation on the WSDL2Apex component of the FuseIT SFDC Explorer. The core idea is to increase support for calling SOAP based web services by generating the required Apex classes from the WSDL.

Breakout Session Blurb

Join us as we review the capabilities of the existing WSDL-to-Apex code generation feature, and explain how we built a tool to provide expanded features using the Tooling API. The resulting tool has greater support for more WSDL features, generates test cases and the associated mocks to maximize code coverage, and optionally includes HttpRequest versions of the callouts.


Using the Tooling API to Generate Apex SOAP Web Service Clients

Offsite Session Video on Vidyard.



The demo's went well. I was caught out a bit with the resolution. I went in expecting 1280x720 and it got bumped up to 1920x1080. Hopefully the core parts scaled well enough to be seen. Note to self, have a magnifier tool handy for future demo's.

It was difficult to see the audience in the breakout room with 3 spot lights pointed at the stage - the cellphone camera isn't really showing how blinding it was. I could hear people were out there, but couldn't really see them. Some of the breakout rooms had this set up and some didn't, I'm not sure why.

These were minor things really, and I'm glad I was given the opportunity to present at Dreamforce. I got some great questions and feedback from the session. We are getting more followup queries at work now a couple of weeks after Dreamforce. With so much going on during the conference it seems to take awhile for people to filter back to work and follow up on sessions. Also, with so much going on, it was tempting to skip the sessions that were being recorded and attend other activities, such as labs and meeting with other attendees.

Saturday, October 25, 2014

Dreamforce 2014 Round-up / Summary

This was my first time attending Dreamforce and subsequently presenting there as well. I focused primarily on the development side of things, so spent most of my time around Moscone West.

I've collected some of my session notes and general conference thoughts here. Content here will evolve over time as I collate all my notes etc...


Sessions

Introducing VF Fiddle

Presenter: @EvilN8. Session Video

As you may know, I'm an active member of the Salesforce StackExchange site. I find the format useful for asking and answering Salesforce specific questions. One of the time consuming things about the site is trying to replicate an askers environment and then communicating the solution back in the answer.

VF Fiddle is similar in concept to JSFiddle and allows for quick deployment of Visualforce markup, the Apex controller, CSS and Javascript to a Dev Org. https://vffiddle.herokuapp.com/


External Objects - Magical External Objects: Seamless Integration is Here

Presenters: Agustina Garcia Peralta and Andrew Fawcett. Session Video

a.k.a Salesforce1 Platform Connect a.k.a. Lightning Connect

External Objects allow you to view and search data from an external system as though is was a native custom object without actually loading all the data into Salesforce.

Configured under Build > Develop > External Objects and ExternalDataSource.

The main advantage of not storing the data in a custom object is that it does not count against the Org storage limits and you don't need to try and keep the data in sync. At this stage data can't be updated in the data source.

Odata Odata limits apply on single round trip size - 4 MB. Total size of continuation links is 8MB.

There is also a pilot Apex Connector API with classes extending DataSource.Connection and DataSource.Provider

Can you query the data in Apex and SOQL? Yes
Used as field relationship? Yes, with some limits - Can't be used in a Master-Detail relationship.
Are updates from Salesforce synced back? Read Only at this stage.

Where a standard custom object has the __c suffix, external objects have the __x suffix.

Limited to 100 External objects per org.

Access External Data with Salesforce1 Platform Connect


Apex Tips And Tricks

Presenter: Gonzalo Abruna. Session Video

SOQL queries often take up most of the CPU time.

The Developer Console Execution Overviews Timeline is a great way to check a transaction to see where the performance issues are. Open the Apex log and use Debug > Switch Perspective > All or Analysis.


Message Queuing - Building a Messaging Framework on the Salesforce1 Platform

Presenter: @anup Slides

An alternative to using Outbound messages using a custom queue implementation. Independent batches that monitor a message queue and invoke the applicable service handle to send the message out and process the response.

   

Build Your Own Platform on Force.com

Presenters: Mac Anderson, Avrom Roy-Faderman, Aaron Slettehaugh. Session Video

This session will provide an overview of Platform on the Platform, including demonstrations by sfdc partners of features built with the first of our three major features: Custom Metadata. Custom Metadata are similar to List Custom Settings except that the records can be packaged, installed, deployed and managed as metadata. We will show how for the first time, sfdc customers and partners will be able to create their own metadata records and your own platform.

__mdt suffix


Proxly - Speed Up Your Force.com Development and Go Localhost

Use local resources for JavaScript and CSS references to speed up development. https://github.com/dnakov/proxly


Flex Queue - Apex Flex Queue: Batch Apex Liberated

Presenter: @CarolEnLaNube. Session Video

Current Situation:

  • @Future No Job ID - returns void - can't monitor - must be static - primitive data types
  • Batch Jobs - returns ID that can be monitored, recursive calls, only 5 active jobs, can't be ordered.
  • Schedule Jobs

Flex Queue - combines best parts of @Future and Batch

Batch Jobs : With FlexQue.

More that 5 Apex Jobs in the Apex Jobs Table
5 Apex Jobs Processing + 95 Apex Jobs in Holding

Can you reorder Jobs from code or give them a priority? Appears not.
Can't abort holding jobs using the UI, need to use System abort.

Queueable interface and QueueableContext in execute. Provides a middle ground between Batch jobs and @future
Supports more than just primitive arguments that @future methods do.
Less overhead than a Batch job as there is no start method to go through the aync queue.
System.enqueueJob() - returns Job ID. So you can monitor progress.
Can be chained together, which future jobs can't be.
Can have 50 Queueable jobs.


Interactive Apex Debugging

Presenter: Josh Kaplan. Session Video.

Due sometime in 2015 as an Eclipse plugin. Sounds like it will be a paid SKU for a sandbox org.


Meet the Developers

Presenters: Steven Tamm, Gregory Fee, Phil Calvin, Doug Chasman

A great session where you can ask questions of the Salesforce developers.


Lightning


Sessions I want to catch up on (among others)

  • Catch Bugs Early with Apex Diagnostic Code
    Ridding your Apex code of pesty bugs improves the quality of your Salesforce implementation and increases the predictability of your application's success. Catching those bugs early saves time and money and having the proper skills to do so when you start development saves even more. Join us as we cover contract programming, assertions, and enhanced logging and notification.
  • A Deep Dive into Debugging Applications on the Salesforce1 Platform
  • Building Machine Learning Systems with Apex
  • Faster SOQL? Yes, Please Slides

FinancialForce DevTalks DF14 - Apex Unit Testing with ApexMocks Framework

I was surprised to hear about the meetup scheduled during Dreamforce. It was a really interesting session on a mocking framework called ApexMocks.


Presenting - Using the Tooling API to Generate Apex SOAP Web Service Clients

I gave a breakout presentation on the WSDL2Apex component of the FuseIT SFDC Explorer that increases support for calling SOAP based web services. See Dreamforce 2014 Presentation - Improved Apex support for SOAP based web services, which includes the session video and slides.


Miscellaneous Notes

Salesforce StackExchange

I had a number of shirts to give out, which was easily accomplished on the first day. It's a great way to get talking to people.

Breakfast, Lunch on site

The on site breakfast on the Monday was a selection of fruit and granola (muesli with more sugar or other sweeteners) bars. Lunch was no longer available in Moscone West or the Yerba Buena Gardens around 12:30 pm on the Monday. Subsequent days I had better luck with the Moscone West expo area around 11:30am. It tended to be prepackaged, so you could grab something and eat it later easily enough.

Gala

There was a choice between Bruno Mars and Cake. The food options were really good inside were Cake were playing.

Taking notes

I'd recently picked up a Surface 2 and already had the type keyboard acccessory to use with it. Combined in Onenote it worked well as an all day not taking device. I was also able to use it to complete some of the labs using Salesforce from the built in IE browser. There were a few qwirks with using the developer console.

Uber

Salesforce gave out a coupon code for first time users of Uber with the $30 credit for the first trip. I set this up with Uber on my phone before leaving New Zealand using my local credit card and cell phone number. On arriving in San Francisco I picked up an AT&T SIM card to avoid excessive international roaming charges. When I tried to book my first Uber ride they sent a confirmations SMS text message to my NZ cell phone number, which I wasn't able to receive due to switching SIM cards. I could have swapped the SIM cards to get the message, but found it too much of a hassle to try and accomplish in the street. My phone needs a pin or similar to pop the SIM card out. So, moral of the story, wait to register for Uber until you get your local SIM if you are going to switch.

Accommodation

I was fortunate enough to have booked accommodation early through the Dreamforce website so I was walking distance to the conference.

One surprise here was the payment going through my personal credit card that I presented at check-in for incidentals rather than the company credit card that was used on the Dreamforce website to request the accommodation.

Tourist Travels

Being my first trip to San Francisco I wanted to get out and do some touristy things. I'd recommend:

  • Hire a bike and ride over the Golden Gate Bride to Sausalito or further on to the Red Woods. I made a day of it and cycled back as well. If I'd known it was there I would have visited the Bay Model Visitor Center.
  • The Alcatraz audio tour.
  • Coit Tower - some good view over the city and harbour.

Thursday, October 23, 2014

Importing the Salesforce Winter 15 Partner API to .NET

After updating the Partner API to v32.0 from v31.0 I started getting the following SGEN compilation errors:

  1. System.InvalidOperationException: Unable to generate a temporary class (result=1).
  2. error CS0029: Cannot implicitly convert type 'XYZ.SalesforcePartner.ListViewRecordColumn' to 'XYZ.SalesforcePartner.ListViewRecordColumn[]'

The new ListViewRecord and ListViewRecordColumn complexTypes from the v32.0 wsdl:

            <complexType name="ListViewRecord">
                <sequence>
                    <element name="columns"                  type="tns:ListViewRecordColumn" maxOccurs="unbounded"/>
                </sequence>
            </complexType>

            <complexType name="ListViewRecordColumn">
                <sequence>
                    <element name="fieldNameOrPath"          type="xsd:string"/>
                    <element name="value"                    type="xsd:string" nillable="true"/>
                </sequence>
            </complexType>

The problem appears in the generated Reference.cs with the ExecuteListViewResult.records multidimensional array return type.

        /// 
        [System.Xml.Serialization.XmlArrayItemAttribute("columns", typeof(ListViewRecordColumn), IsNullable=false)]
        public ListViewRecordColumn[][] records {
            get {
                return this.recordsField;
            }
            set {
                this.recordsField = value;
            }
        }

The XmlArrayItemAttribute typeof(ListViewRecordColumn) should be typeof(ListViewRecordColumn[]). After changing this manually the web reference compiled again.

        /// 
        [System.Xml.Serialization.XmlArrayItemAttribute("columns", typeof(ListViewRecordColumn[]), IsNullable=false)]
        public ListViewRecordColumn[][] records {
            get {
                return this.recordsField;
            }
            set {
                this.recordsField = value;
            }
        }

See also:

Wednesday, October 8, 2014

How to contribute to the Salesforce WSDL2Apex project

WSDL2Apex is a Salesforce tool for converting a web services WSDL file into Apex classes that can make the required SOAP callouts and then process the responses. Internally the callouts are made using the Apex class WebServiceInvoke. In September 2014 it was released as an open source project.

I'll cover setting up a development environment to build it and then contribute those changes back in this post. It is loosely based on Peter Knolle's Extending the Force.com IDE article.

  • Clone the project repository from GitHub. The Fork button on the web page made me a clone easily enough at https://github.com/FishOfPrey/WSDL2Apex
  • If you don't have it already, get the Java Development Kit 7. For me this installed as %Program Files%\Java\jdk1.7.0_67.
  • Add the Java 7 JDK bin to the PATH.
    ...\WSDL2Apex\WSDL2Apex-master\target>SET PATH=%PATH%;C:\Program Files (x86)\Java\jdk1.7.0_67\bin
  • Get Apache Maven 3.2.2 to manage the builds and test runs.
  • Setup the Maven PATH requirements as per the Installation instructions.
    > SET JAVA_HOME=C:\Program Files (x86)\Java\jdk1.7.0_67

    Note the absence of surrounding double quotes. With quotes I was getting the message "Files was unexpected at this time."
    > SET PATH=%PATH%;D:\Development\Maven\apache-maven-3.2.3\bin
  • Package it up, including running the test cases:
    >mvn install package
  • Run WSDL2Apex from the command line:
    ...\WSDL2Apex\WSDL2Apex-master\target>java -jar WSDL2Apex-1.0.jar "C:\PathTo\WSDLs\WithLimitElement.xml" outputDir yes

You will need to submit a Contributor's License Agreement before Salesforce can accept your pull requests. The form is available on the Contributing Code page.

It is licensed under the Eclipse Public License (EPL) v1.0. Eclipse Public License (EPL) Frequently Asked Questions is a useful resource.

See also:

Friday, September 26, 2014

Prototype CSV viewer for Visualforce

This is a quick prototype to pull a CSV out of a Document blob stored in Salesforce and render it in a table via Visualforce. It borrows heavily from the CSV parser by Marty Chang linked at the end of the post.

Parts are a bit rough around the edges. E.g. It is missing standard error checking.

CsvReader.apex

/**
 * Slightly modified CSV parser
 * Originally by MARTY Y. CHANG: 
 *  http://frombelvideres4thfloor.blogspot.com.es/2010/10/ietf-rfc-4180-compliant-csv-reader-for.html
 */
public with sharing class CsvReader {
  
    /**
     * Comma String as defined by IETF RFC 4180.
     */
    public static final String ParserCOMMA = String.fromCharArray(new List<Integer> { 44 });

    /**
     * Carriage return String as defined by Salesforce documentation.
     *
     * Force.com IDE Library >
     * Apex Developer's Guide >
     * Language Constructs >
     * Data Types >
     * Primitive Data Types
     */
    public static final String ParserCR = '\r';
    
    /**
     * Double-quote String as defined by Salesforce documentation.
     *
     * Force.com IDE Library >
     * Apex Developer's Guide >
     * Language Constructs >
     * Data Types >
     * Primitive Data Types
     */
    public static final String ParserDQUOTE = '\"';
    
    /**
     * Line feed String as defined by Salesforce documentation.
     *
     * Force.com IDE Library >
     * Apex Developer's Guide >
     * Language Constructs >
     * Data Types >
     * Primitive Data Types
     */
    public static final String ParserLF = '\n';
    
    /**
     * Carriage return String followed by a line feed String.
     */
    public static final String ParserCRLF = ParserCR + ParserLF;
    
    /**
     * Line feed String followed by a carriage return String.
     */
    public static final String ParserLFCR = ParserLF + ParserCR;
  
    /**
     * Escaped double-quotes per IETF RFC 4180.
     */
    public static final String ParserDQUOTEDQUOTE = ParserDQUOTE + ParserDQUOTE;

 /**
     * Returns a List containing Lists of Strings that represents
     * the values contained in an IETF RFC 4180-compliant CSV file.
     *
     * Each element in the outer list represents a row in the CSV file.
     * Each element in the inner list is the value in the field specified
     * by the row-column combination.
     *
     * @param  file the CSV file to read
     * @return      the List<List<String>> containing values read from the
     *              CSV file
     */
    public static List<List<String>> readIETFRFC4180CSVFile(Blob file) {
        String fileString = file.toString();
        
        if (!fileString.endsWith(ParserCRLF)) {
          fileString = fileString + ParserCRLF;
        }
        
        List<List<String>> fileValues = new List<List<String>>();
        List<String> rowValues = new List<String>();
        CSVValue csvValue = new CSVValue();
        
        Boolean eod = false;  // Whether end of CSV data is reached
        while (!eod) {
          System.debug(fileString);
          
            csvValue = readIETFRFC4180CSVValue(fileString);
            
            rowValues.add(csvValue.value);
            
            if (csvValue.delimiter == ParserCRLF) {
              fileValues.add(rowValues);
              
              System.debug(rowValues);
              
              if (fileValues.size() > 0) {
                System.assertEquals(fileValues.get(0).size(),
                      rowValues.size());
              }
              
              rowValues = new List<String>();
            }
            
            if (csvValue.biteSize() == fileString.length()) {
              eod = true;
            }
            else {
              fileString = fileString.substring(csvValue.biteSize());
            }
        }
        
        return fileValues;
    }
    
    /**
     * Returns the first String value read from a String representation of
     * data contained in an IETF RFC 4180-compliant CSV file.
     *
     * The data is assumed to be terminated with a CRLF.
     *
     * @param  data the textual CSV data in one long string
     * @return      the first CSV value read from <code>data</code>.
     *              null is returned if no value is discerned.
     */
    public static CSVValue readIETFRFC4180CSVValue(String data) {
        System.assert(data.endsWith(ParserCRLF));
        
        CSVValue csvValue = new CSVValue();
        
        if (data.startsWith(ParserDQUOTE)) {
          csvValue.enclosed = true;
          
            Integer searchIndex = 1;      // starting index to search
            Integer dquoteIndex = -1;     // index of DQUOTE
            Integer dquotesIndex = -1;    // index of DQUOTEDQUOTE
                            
            Boolean closerFound = false;
            
            while (!closerFound) {
                dquoteIndex = data.indexOf(ParserDQUOTE, searchIndex);
                
                dquotesIndex = data.indexOf(ParserDQUOTEDQUOTE,
                        searchIndex);
                
                System.assert(dquoteIndex != -1);
                
                if (dquoteIndex == dquotesIndex) {
                    searchIndex = dquotesIndex
                            + ParserDQUOTEDQUOTE.length();
                }
                else {
                    closerFound = true;
                }
            }
            
            csvValue.value = data.substring(
                    ParserDQUOTE.length(), dquoteIndex)
                            .replaceAll(ParserDQUOTEDQUOTE, ParserDQUOTE);
            
            Integer commaIndex = data.indexOf(ParserCOMMA, dquoteIndex);
            Integer crlfIndex = data.indexOf(ParserCRLF, dquoteIndex);
            
            if (commaIndex != -1 && commaIndex < crlfIndex) {
                csvValue.delimiter = ParserCOMMA;
            }
            else {
                csvValue.delimiter = ParserCRLF;
            }
        }
        else {
          csvValue.enclosed = false;
          
            Integer commaIndex = data.indexOf(ParserCOMMA);
            Integer crlfIndex = data.indexOf(ParserCRLF);
            
            if (commaIndex != -1 && commaIndex < crlfIndex) {
                csvValue.value = data.substring(0, commaIndex);
                csvValue.delimiter = ParserCOMMA;
            }
            else {
                csvValue.value = data.substring(0, crlfIndex);
                csvValue.delimiter = ParserCRLF;
            }
        }
        
        System.debug('Returning: ' + csvValue);
        
        return csvValue;
    }
    
    /**
     * CSVValue is a class structure containing information about a CSV
     * value that was read from a CSV file, including such information as
     * whether the value was encapsulated in double-quotes.
     */
    private class CSVValue {
        /**
         * The field value that was read from the CSV file.
         */
        public String value;
        
        /**
         * Whether the value was surrounded by double-quotes.
         */
        public Boolean enclosed;
        
        /**
         * The comma or CRLF delimiter that identified the end of the CSV value.
         */
        public String delimiter;
        
        /**
         * Default constructor, setting all members to null.
         */
        public CSVValue() {
            this(null, null, null);
        }
        
        /**
         * Constructor.
         *
         * @param value     the field value
         * @param enclosed  whether the value was surrounded by double-quotes
         * @param delimiter the delimiter that identified the end
         *                  of the CSV value
         */
        public CSVValue(String value, Boolean enclosed, String delimiter) {
            this.value = value;
            this.enclosed = enclosed;
            this.delimiter = delimiter;
        }
        
        /**
         * Returns the number of characters to remove from the data
         * String which produced the CSVValue in order to reach the next
         * value in the data String.
         */
        public Integer biteSize() {
          Integer biteSize = value
                 .replaceAll(ParserDQUOTE, ParserDQUOTEDQUOTE).length()
                         + delimiter.length();
          
          if (enclosed) {
            biteSize += ParserDQUOTE.length() * 2;
          }
          
          System.debug('biteSize: ' + biteSize);
          
          return biteSize;
        }
        
        /**
         * Returns whether a CSVValue has the same <code>value</code> and
         * <code>enclosed</code> as another CSVValue.
         */
        public Boolean equals(CSVValue compCSVValue) {
            return this.value.equals(compCSVValue.value)
                    && this.enclosed == compCSVValue.enclosed
                            && this.delimiter == compCSVValue.delimiter;
        }
        
        /**
         * Asserts that two <code>CSVValue</code> instances have the same
         * <code>value</code> and <code>enclosed</code>.
         */
        public void assertEquals(CSVValue compCSVValue) {
            System.assertEquals(value, compCSVValue.value);
            System.assertEquals(enclosed, compCSVValue.enclosed);
            System.assertEquals(delimiter, compCSVValue.delimiter);
        }
    }
    
    /**
     * Test some use cases for reading IETF RFC 4180-compliant CSV values.
     */
    /* TODO: Move to a separate class
    public static testMethod void readIETFRFC4180CSVValueTest() {
        String data = null;  // Placeholder for data to use in testing.
        
        System.debug(data = ParserCRLF);
        new CSVValue('', false, ParserCRLF)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = '""' + ParserCRLF);
        new CSVValue('', true, ParserCRLF)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = '"",asdf' + ParserCRLF);
        new CSVValue('', true, ParserCOMMA)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = ',asdf' + ParserCRLF);
        new CSVValue('', false, ParserCOMMA)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = '"' + ParserCRLF + '",blah' + ParserCRLF);
        new CSVValue(ParserCRLF, true, ParserCOMMA)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = '"""marty""","""chang"""' + ParserCRLF);
        new CSVValue('"marty"', true, ParserCOMMA)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = '"com""pli""cate' + ParserCRLF + 'd"'
                + ParserCRLF);
        new CSVValue('com"pli"cate' + ParserCRLF + 'd', true, ParserCRLF)
                .assertEquals(readIETFRFC4180CSVValue(data));
        
        System.debug(data = 'asdf' + ParserCRLF);
        new CSVValue('asdf', false, ParserCRLF)
                .assertEquals(readIETFRFC4180CSVValue(data));
    }
    */

}

ViewCsvController.apex

public with sharing class ViewCsvController {
 
 public List<string> header_row { get; set; }
 public Map<integer, List<string>> row_map { get; set; }

 public ViewCsvController() {
  // Sample data that can be used if there isn't a document available.
  /*
  header_row = new List<string>{'Column 1', 'Column 2'};
  
  row_map = new Map<integer, List<string>>();
  row_map.put(1, new List<string>{'AAA', 'BBB'});
  row_map.put(2, new List<string>{'CCC', 'DDD'});
  */

  Id documentId = ApexPages.currentPage().getParameters().get('docId');
  if(documentId == null) {
   documentId = '015400000000001';
  }
  List<Document> docs = [Select Id, Body from Document where Id = :documentId];
                // TODO: Check that the list is not empty.

  List<List<String>> fileValues = CsvReader.readIETFRFC4180CSVFile(docs[0].Body);

  header_row = fileValues[0];
  
  row_map = new Map<integer, List<string>>();
  for(integer i = 1; i < fileValues.size(); i++) {
   row_map.put(i, fileValues[i]);
  }

 }
}

ViewCsv.page

<apex:page showHeader="true" sidebar="true" controller="ViewCsvController">
 <table border="1">
  <tr>
   <apex:repeat value="{!header_row}" var="h_col">
    <td><b>{!h_col}</b></td>
   </apex:repeat>
  </tr>


  <apex:repeat value="{!row_map}" var="idx">
  <tr>
   <apex:repeat value="{!row_map[idx]}" var="r_col">
    <td>{!r_col}</td>
   </apex:repeat>
  </tr>
  </apex:repeat>

 </table>
</apex:page>

See also:

Monday, June 23, 2014

Salesforce Log Categories and Events by Level


Please see the 2018 version of this post - Salesforce Log Categories and Events by Level - Revisited


The following table shows the Apex logging events that occur at each logging level by logging category. Logging events from higher levels also appear in all the lower levels.

The data is similar to that available through the Salesforce page that sets the debug log filters. That page shows the events dynamically based on the selected level in each category. Here I've currently gone for a static approach to make it more searchable.

The table is currently really wide and will probably be hard to read on lower resolution screens. I'll play around with it over time to see if the layout can be improved.

Logging Level / Category System Visualforce Apex Profiling Apex Code Callout Validation Workflow Database
ERROR
  • USER_DEBUG[LoggingLevel.Error]
  • CODE_UNIT_STARTED, CODE_UNIT_FINISHED
  • EXECUTION_STARTED, EXECUTION_FINISHED
  • FATAL_ERROR
  • PUSH_NOTIFICATION_INVALID_CERTIFICATE
  • PUSH_NOTIFICATION_INVALID_APP
  • PUSH_NOTIFICATION_INVALID_NOTIFICATION
  • WF_FLOW_ACTION_ERROR
  • WF_FLOW_ACTION_ERROR_DETAIL
  • FLOW_CREATE_INTERVIEW_ERROR
  • FLOW_START_INTERVIEWS_ERROR
  • FLOW_ELEMENT_ERROR
WARN
  • USER_DEBUG[LoggingLevel.Warn]
  • FLOW_ELEMENT_FAULT
INFO
  • POP_TRACE_FLAGS, PUSH_TRACE_FLAGS
  • SYSTEM_MODE_ENTER, SYSTEM_MODE_EXIT
  • DUPLICATE_DETECTION_BEGIN, DUPLICATE_DETECTION_END
  • DUPLICATE_DETECTION_RULE_INVOCATION
  • DUPLICATE_DETECTION_MATCH_INVOCATION_SUMMARY
  • MATCH_ENGINE_BEGIN, MATCH_ENGINE_END
  • MATCH_ENGINE_INVOCATION
  • VF_SERIALIZE_VIEWSTATE_BEGIN, VF_SERIALIZE_VIEWSTATE_END
  • VF_DESERIALIZE_VIEWSTATE_BEGIN, VF_DESERIALIZE_VIEWSTATE_END
  • VF_SERIALIZE_CONTINUATION_STATE_BEGIN, VF_SERIALIZE_CONTINUATION_STATE_END
  • VF_DESERIALIZE_CONTINUATION_STATE_BEGIN, VF_DESERIALIZE_CONTINUATION_STATE_END
  • CUMULATIVE_LIMIT_USAGE, CUMULATIVE_LIMIT_USAGE_END
  • TESTING_LIMITS
  • USER_DEBUG[LoggingLevel.Info]
  • EMAIL_QUEUE
  • ENTERING_MANAGED_PKG
  • EXCEPTION_THROWN
  • VF_APEX_CALL
  • VF_PAGE_MESSAGE
  • BULK_COUNTABLE_STATEMENT_EXECUTE
  • HEAP_DUMP
  • SCRIPT_EXECUTION
  • PUSH_NOTIFICATION_NOT_ENABLED
  • CALLOUT_REQUEST, CALLOUT_RESPONSE
  • VALIDATION_ERROR
  • VALIDATION_FAIL
  • VALIDATION_FORMULA
  • VALIDATION_PASS
  • VALIDATION_RULE
  • SLA_END
  • SLA_EVAL_MILESTONE
  • SLA_NULL_START_DATE
  • SLA_PROCESS_CASE
  • WF_ACTION
  • WF_ACTION_TASK
  • WF_ACTIONS_END
  • WF_APPROVAL
  • WF_APPROVAL_REMOVE
  • WF_APPROVAL_SUBMIT
  • WF_ASSIGN
  • WF_CRITERIA_BEGIN
  • WF_CRITERIA_END
  • WF_EMAIL_ALERT
  • WF_EMAIL_SENT
  • WF_ENQUEUE_ACTIONS
  • WF_ESCALATION_ACTION
  • WF_ESCALATION_RULE
  • WF_EVAL_ENTRY_CRITERIA
  • WF_FIELD_UPDATE
  • WF_FORMULA
  • WF_HARD_REJECT
  • WF_NEXT_APPROVER
  • WF_NO_PROCESS_FOUND
  • WF_OUTBOUND_MSG
  • WF_PROCESS_NODE
  • WF_REASSIGN_RECORD
  • WF_RESPONSE_NOTIFY
  • WF_RULE_ENTRY_ORDER
  • WF_RULE_EVAL_BEGIN
  • WF_RULE_EVAL_END
  • WF_RULE_EVAL_VALUE
  • WF_RULE_FILTER
  • WF_RULE_INVOCATION
  • WF_RULE_NOT_EVALUATED
  • WF_SOFT_REJECT
  • WF_SPOOL_ACTION_BEGIN
  • WF_TIME_TRIGGER
  • WF_TIME_TRIGGERS_BEGIN
  • WF_KNOWLEDGE_ACTION
  • WF_SEND_ACTION
  • WF_CHATTER_POST
  • WF_QUICK_CREATE
  • WF_FLOW_ACTION_BEGIN
  • WF_FLOW_ACTION_END
  • WF_APEX_ACTION
  • FLOW_CREATE_INTERVIEW_BEGIN
  • FLOW_CREATE_INTERVIEW_END
  • FLOW_START_INTERVIEWS_BEGIN
  • FLOW_START_INTERVIEWS_END
  • FLOW_START_INTERVIEW_BEGIN
  • FLOW_START_INTERVIEW_END
  • DML_BEGIN, DML_END
  • QUERY_MORE_ITERATIONS
  • SAVEPOINT_SET, SAVEPOINT_ROLLBACK
  • SOQL_EXECUTE_BEGIN, SOQL_EXECUTE_END
  • SOSL_EXECUTE_BEGIN, SOSL_EXECUTE_END
DEBUG
  • DUPLICATE_DETECTION_MATCH_INVOCATION_DETAILS
  • USER_DEBUG[LoggingLevel.Debug - Default]
  • PUSH_NOTIFICATION_NO_DEVICES
  • PUSH_NOTIFICATION_SENT
FINE
  • SYSTEM_CONSTRUCTOR_ENTRY, SYSTEM_CONSTRUCTOR_EXIT
  • SYSTEM_METHOD_ENTRY, SYSTEM_METHOD_EXIT
  • CUMULATIVE_PROFILING
  • CUMULATIVE_PROFILING_BEGIN, CUMULATIVE_PROFILING_END
  • STACK_FRAME_VARIABLE_LIST
  • STATIC_VARIABLE_LIST
  • TOTAL_EMAIL_RECIPIENTS_QUEUED
  • USER_DEBUG[LoggingLevel.Fine]
  • CONSTRUCTOR_ENTRY, CONSTRUCTOR_EXIT
  • METHOD_ENTRY, METHOD_EXIT
  • FLOW_BULK_ELEMENT_BEGIN
  • WF_FLOW_ACTION_DETAIL
  • FLOW_ELEMENT_BEGIN
  • FLOW_ELEMENT_END
  • FLOW_BULK_ELEMENT_BEGIN
  • FLOW_BULK_ELEMENT_END
FINER
  • VF_EVALUATE_FORMULA_BEGIN, VF_EVALUATE_FORMULA_END
  • USER_DEBUG[LoggingLevel.Finer]
  • HEAP_ALLOCATE, HEAP_DEALLOCATE
  • STATEMENT_EXECUTE
  • FLOW_ASSIGNMENT_DETAIL
  • FLOW_BULK_ELEMENT_DETAIL
  • FLOW_SUBFLOW_DETAIL
  • FLOW_RULE_DETAIL
  • FLOW_VALUE_ASSIGNMENT
  • FLOW_LOOP_DETAIL
FINEST
  • LIMIT_USAGE
  • LIMIT_USAGE_FOR_NS
  • USER_DEBUG[LoggingLevel.Finest]
  • BULK_HEAP_ALLOCATE
  • VARIABLE_ASSIGNMENT
  • VARIABLE_SCOPE_BEGIN, VARIABLE_SCOPE_END
  • CALLOUT_REQUEST_PREPARE
  • CALLOUT_REQUEST_FINALIZE
  • IDEAS_QUERY_EXECUTE
  • QUERY_MORE_BEGIN, QUERY_MORE_END

Special Cases:

  1. USER_DEBUG
  2. MAXIMUM_DEBUG_LOG_SIZE_REACHED

See also:

Monday, April 14, 2014

Web Deployment Made Easy: If You're Using Web.config transformations, You're Doing it Wrong

With all due respect to Scott Hanselman and his Web Deployment Made Awesome: If You're Using XCopy, You're Doing It Wrong post, using Config Transforms can represent a significant amount of work over just copying a separate file with the complete config for each deployment environment.

There is something to be said for having a file that contains the entire config file as it will be deployed to the server. For one, it is easy to run through a Diff tool and see how the server configuration differs from a local build without first having to do a Preview Transform command.

To be fair, the following is certainly the wrong way to be approaching XML transforms. It is however quick and easy to do. Especially when migrating an existing large project. I didn't need to go through the configs line be line to diff between the local build, the shared dev environment, the staging/QA environments, and the production environments. I just copied each complete config into its separate build configuration based config file and carried on with my life.

Overtime I'll probably revisit each configuration and work towards defining the numerous differences between each environment.

<?xml version="1.0" encoding="utf-8"?>

<!-- For more information on using web.config transformation visit http://go.microsoft.com/fwlink/?LinkId=125889 -->

<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform" xdt:Transform="Replace">
    <!-- Dump the entire contents of the configuration element from the other .config file here -->
    <configSections>
        <!-- ... -->
    </configSections>
    <appSettings>
        <!-- ... -->
    </appSettings>
    <connectionStrings>
        <!-- ... -->
    </connectionStrings>
    <system.web>
        <!-- ... -->
    </system.web>
    <!-- and so on... -->
</configuration>

Monday, March 24, 2014

An alternative to Salesforces Wsdl2Apex for calling SOAP web services in Apex

Salesforce provides a tool called Wsdl2Apex that allows you to generate Apex classes from a WSDL. These Apex classes act as a proxy for invoking the web service methods.

While functional, Wsdl2Apex has a number of limitations. Including:

  1. The generated Apex classes require code coverage, which needs to be created manually.
  2. You need to import the entire WSDL. In many cases you may only require a subset of the web methods. Reducing the number of methods cuts down the lines of Apex (a limited resource) that are generated and subsequently the number of lines requiring code coverage.
  3. Support for complex types that extend a base type. <xsd:extension base="foo:Bar">.
  4. Support for importing another WSDL. <xsd:import>
  5. Support for attributes. <xsd:attribute>
  6. The ordering of the generated methods appears to be arbitrary (maybe hash based ordering internally?). At the very least, a small change in the input WSDL can produce Apex that doesn't diff very well. This can be a pain with source control and tracking the history of changes.
  7. Conflicts between named WSDL elements and reserved keywords in Apex. E.g. long
  8. WSDLs that contain multiple wsdl:binding elements

I've been working with an intern student here at FuseIT to create a replacement tool as part of the FuseIT SFDC Explorer under the new WSDL tab in the 1.0.0.47 release. The current release is aimed at having reasonable feature parity with the existing Salesforce Wsdl2Apex implmentation.

Current functionality:

  • Import a WSDL from URL or local file.
  • User definable class names for each WSDL namespace
  • Detection of existing Apex Classes that match those being generated
  • The ability to select just the Apex methods that should be generated (including a description of the input and output parameters)
  • Publish the Apex classes directly into a Salesforce Org via the Tooling API.

Work in progress and possible future extensions:

  • Generate test Apex methods and mocks that give 100% code coverage for the generated callout code.
  • Generate HttpRequest web service calls as an alternative/backup to the WebServiceCallout.invoke calls.
  • Generate a WebServiceMock class with expanded request/response objects and doInvoke implementation.
  • Generate a wrapper Apex class that will revert to the mock implementation with running in a test case context. This could also expose the end point and timeout settings as properties.

See also:

Wednesday, March 19, 2014

Checking Salesforce ApexClass code coverage using the Tooling API

The FuseIT SFDC Explorer has been extended to provide Code Coverage reports for Apex classes.

Background

It used to be that you could easily bring up the Code coverage for a class from the Setup > Develop > Apex Classes page. There was a column that showed the percentage per class. Clicking this column would open a page that showed the coverage status per line. This was great as you could sort by this column to find classes that with lacking in coverage. Either by percentage or based on the "Size Without Comments" of the apex class. Also, the page that came up could easily be linked to and refreshed with an F5 in the browser. The URL had the format:

https://pod.salesforce.com/setup/build/viewCodeCoverage.apexp?id=01pE00000000001

Then, in the Winter 14 release they stripped this functionality out and proposed using the Developer Console instead.

Sadly, the current response from Product Management is that this useful column and page aren't coming back any time soon:

Josh Kaplan

We are slowly moving all functionality in the various setup pages into the Developer Console. Starting with the Winter '14 release, you will be able to see your code coverage metrics, at a glance, in the Developer Console tool only. This information will no longer be available in the class and trigger list views as it has been in the past.

Moving forward, the Developer Console will be the supported tool for browser-based development on the platform. It is costly to support multiple tools that perform the same function, so we are migrating everything to a single application. Over the next few releases, we will be retiring the old setup pages entirely.

You can still check the code coverage using the developer console, but I find this doesn't work well with my development. Finding the correct class and refreshing test results is ackward. Maybe it's just me and I'm missing some shortcuts with the Developer Console.

Code Coverage with the FuseIT SFDC Explorer

Happily, the Tooling API can now pull most of the required data for code coverage. So, rather than complain about lost functionality, I've started to build my own tool.

The Code Coverage tab in the SFDC Explorer is fairly minimal at this stage. You can search your Apex classes and then open up a code coverage view.

The code coverage results here rely on the results of running asynchronous test cases. You won't see any results from the synchronous test runs.

Given an Apex Class name or Salesforce Id (01p key prefix) you can quickly search for the code coverage results.

There are buttons to link to the Test History in the Salesforce Web UI or clear the current code coverage results.