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

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
  • SYSTEM_CONSTRUCTOR_ENTRY, SYSTEM_CONSTRUCTOR_EXIT
  • SYSTEM_METHOD_ENTRY, SYSTEM_METHOD_EXIT
  • DUPLICATE_DETECTION_MATCH_INVOCATION_DETAILS
  • USER_DEBUG[LoggingLevel.Debug - Default]
  • CONSTRUCTOR_ENTRY, CONSTRUCTOR_EXIT
  • METHOD_ENTRY, METHOD_EXIT
  • PUSH_NOTIFICATION_NO_DEVICES
  • PUSH_NOTIFICATION_SENT
FINE
  • CUMULATIVE_PROFILING
  • CUMULATIVE_PROFILING_BEGIN, CUMULATIVE_PROFILING_END
  • STACK_FRAME_VARIABLE_LIST
  • STATIC_VARIABLE_LIST
  • TOTAL_EMAIL_RECIPIENTS_QUEUED
  • USER_DEBUG[LoggingLevel.Fine]
  • 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.

Monday, December 2, 2013

Chrome v30 blocking HTTP Salesforce Web Tab

As of Chrome v30 if you have an http page configured as the URL in a Salesforce Web Tab you will be greated with a blank page.

Checking the developer console shows that the content has been blocked.

[blocked] The page at https://na5.salesforce.com/servlet/servlet.Integration?lid=01r700000000001&ic=1 ran insecure content from http://localhost:60000/site/SalesforceLanding.aspx?SessionId=00D70000000000….

The best solution is to switch the iframe page to use SSL (HTTPS) and you won't have any further issues.

If the iframe is only for development purposes you can temporarily bypass this security check using a small shield that appears on the right of the address bar and selecting "load unsafe script"

Friday, October 18, 2013

Importing the Salesforce Winter 13 Metadata API to .NET

After updating the Metadata API to v29.0 from v28.0 I started getting the following SGEN compilation errors:

  1. Error 25 Unable to generate a temporary class (result=1). D:\...\SGEN
  2. Error 26 Cannot convert type 'XYZ.SalesforceMetadata.QuickActionLayoutItem[]' to 'XYZ.SalesforceMetadata.QuickActionLayoutItem' D:\...\SGEN
  3. Error 27 Cannot convert type 'XYZ.SalesforceMetadata.QuickActionLayoutItem[]' to 'XYZ.SalesforceMetadata.QuickActionLayoutItem' D:\...\SGEN

The QuickActionLayoutItem complexType from the v29.0 wsdl:

   <xsd:complexType name="QuickActionLayout">
    <xsd:sequence>
     <xsd:element name="layoutSectionStyle" type="tns:LayoutSectionStyle"/>
     <xsd:element name="quickActionLayoutColumns" minOccurs="0" maxOccurs="unbounded" type="tns:QuickActionLayoutColumn"/>
    </xsd:sequence>
   </xsd:complexType>
   <xsd:complexType name="QuickActionLayoutColumn">
    <xsd:sequence>
     <xsd:element name="quickActionLayoutItems" minOccurs="0" maxOccurs="unbounded" type="tns:QuickActionLayoutItem"/>
    </xsd:sequence>
   </xsd:complexType>
   <xsd:complexType name="QuickActionLayoutItem">
    <xsd:sequence>
     <xsd:element name="emptySpace" minOccurs="0" type="xsd:boolean"/>
     <xsd:element name="field" minOccurs="0" type="xsd:string"/>
     <xsd:element name="uiBehavior" minOccurs="0" type="tns:UiBehavior"/>
    </xsd:sequence>
   </xsd:complexType>

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

        /// 
        [System.Xml.Serialization.XmlArrayItemAttribute("quickActionLayoutItems",
              typeof(QuickActionLayoutItem), IsNullable=false)]
        public QuickActionLayoutItem[][] quickActionLayoutColumns {
            get {
                return this.quickActionLayoutColumnsField;
            }
            set {
                this.quickActionLayoutColumnsField = value;
            }
        }

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

        /// 
        [System.Xml.Serialization.XmlArrayItemAttribute("quickActionLayoutItems",
              typeof(QuickActionLayoutItem[]), IsNullable=false)]
        public QuickActionLayoutItem[][] quickActionLayoutColumns {
            get {
                return this.quickActionLayoutColumnsField;
            }
            set {
                this.quickActionLayoutColumnsField = value;
            }
        }

See also: