Tuesday, March 29, 2011

Modifying TFS build server output to seperate project outputpaths

By default a TFS build server outputs all projects in a solution, with the exception of web applications, into a single directory.

To separate the build output directoyr import a modified version of Microsoft.WebApplication.targets into the applicable csproj files.

<Import Project="Modified.Microsoft.WebApplication.targets" />

See also:

Friday, March 25, 2011

APEX String.format(); Syntax - escaping single quotes

The Apex String method format() is my preferred way to build up a string in the absence of something like a .NET StringBuilder.

The help docs are currently a bit lightweight on detail:

Treat the current string as a pattern that should be used for substitution in the same manner as apex:outputText.

The apex:outputText documentation says:

The value attribute supports the same syntax as the MessageFormat class in Java. See the MessageFormat class JavaDocs for more information.

The syntax in Eclipse appears as: String.format(String pString, List pLIST:String) String

Usage is a format string followed by the substitution arguments as an array.

String formattedString = String.format('Hello {0}, shall we play a {1}?', new String[]{'David', 'game'});
System.debug(formattedString);

Apex String.format and escaped single quotes

String.format(); can be a bit fiddly when it comes to outputting single quotes. A solitary escaped single quote will be lost from the output and prevent further string substitutions.

For Example:

String formattedString = String.format('Hello {0}, shall we play a \'{1}\'?', new String[]{'David', 'game'});
System.debug(formattedString);

Will result in:

Hello David, shall we play a {1}?

The java.text.MessageFormat documentation says:

Within a String, "''" represents a single quote. A QuotedString can contain arbitrary characters except single quotes; the surrounding single quotes are removed.

Example with the escaping to produce the expected output:

String formattedString = String.format('Hello {0}, shall we play a \'\'{1}\'\'?', new String[]{'David', 'game'});  
System.debug(formattedString); 

Will result in:

Hello David, shall we play a 'game'?

See Also:

Thursday, March 10, 2011

Find the length of a Salesforce field in Apex

Usually through the Partner API I can use describeSObject() to determine the maximum allowed length of a field.

To do this in Apex use the following - changing CustObj__c and CustField__c as required:

integer fieldLength = Schema.SObjectType.CustObj__c.fields.CustField__c.getLength();

See Also:

Comparing APEX Datetime instances

Something odd is happening with Datetime values in APEX automated tests.

At the start of an automated test case the current date and time is captured using:

  DateTime testStart = DateTime.now();

Then after a number of operations an Account is created. The automated test checks that the CreatedDate of the new Account (after insertion) is greater than when the automated test started.

System.assert(account.CreatedDate > testStart, 'New Account expected - Created Date['+account.CreatedDate+'] <= testStart date['+testStart+'].');

This test assertion fails with the CreatedDate and testStart having exactly the same value according to the assert message.

Converting the Datetimes to longs shows the testStart has more precision than the CreatedDate. It would appear that DateTimes stored in the database lose the millisecond precision.

System.assert(account.CreatedDate.getTime() > testStart.getTime(), 'New Account expected - Created Date['+account.CreatedDate.getTime()+'] < testStart date['+testStart.getTime()+'].');

Friday, March 4, 2011

Consuming an ASP.NET Web Service from Salesforce Apex Callouts

Salesforces wsdl2apex can be a bit basic in what is supports (see Supported WSDL Features).

Steps for consuming an ASP.NET web service from Apex.

  1. Add the Web service host to the Remote Sites.
    Adminitraction Setup > Security Controls > Remote Site Settings
    Skipping this step will result in a "System.CalloutException: IO Exception: Unauthorized endpoint, please check Setup->Security->Remote site settings. endpoint ="...
  2. Export the WSDL from the ASP.NET web service
  3. Optionally, you might like to sort the WSDL. The idea is to ensure wsdl2apex produces the same elements in the same order for future code comparisons.
  4. Import the WSDL into Salesforce
    Develop > Apex Classes > Generate from WSDL*
  5. Define a Apex class name
  6. Test the new Apex class using Execute Anonymous in Eclipse. With a ASP.NET generated web service I found the class I defined in the previous step had a nested class corresponding to the binding name from the uploaded WSDL. The nested class had a method that corresponded to the web method (the operation in the WSDL).
  7. Optionally, search the generated class for calls to WebServiceCallout.invoke(...). This will currently cause test cases to abort silently (except for a log message) and you may not notice. Sprinkle the code with a liberal amount of Test.isRunningTest().

*Common WSDL parsing errors:

  1. If the Parse WSDL button produces Error: Failed to parse wsdl: Found more than one wsdl:binding. WSDL with multiple binding not supported Deleting the second <wsdl:binding> element what contains soap12 elements and the corresponding port definition under <wsdl:wervice>.
  2. wsdl:import
    Error: Failed to parse wsdl: Unknown element: import
    Locate the import elements in the current WSDL. E.g. . Pull that WSDL down and merge its contents into the parent WSDL.
  3. xsd:import
    Failed to parse wsdl: Found schema import from location http://example.com/WebService.svc?xsd=xsd0. External schema import not supported
    These will typically appear under <wsdl:types>. Copy the contents of the import under <wsdl:types> and remove the <xsd:schema>. See also: Force.com Discussion Forum: Referencing external schemas in a WSDL
  4. Error: Failed to parse wsdl: Failed to parse WSDL: Unable to find binding {http://tempuri.org/}BasicHttpBinding_IWebService. Found BasicHttpBinding_IWebService instead.
    The <wsdl:definitions> had the attribute xmlns:i0="http://tempuri.org/". i0: was being used as the namespace for the binding. Switching it to tns: resolved the issue.
  5. Error: Failed to parse wsdl: schema:targetNamespace can not be null
    In this case I needed to add a targetNamespace attribute and value to the element. E.g. <xsd:schema> becomes <xsd:schema targetNamespace="http://www.example.com/api/Imports">
  6. Apex Generation Failed:
    Unable to find schema for element; {http://schemas.microsoft.com/2003/10/Serialization/Arrays}ArrayOfstring
    and
    Unable to find schema for element; {http://schemas.microsoft.com/2003/10/Serialization/Arrays}ArrayOflong
    In this case I'd initially made a mistake when pulling in the xsd for these complex types. I corrected it by putting the complex type definitions within a <xsd:schema targetNamespace="http://schemas.microsoft.com/2003/10/Serialization/Arrays">element in wsdl:type section.
  7. Error: wwwExampleComApi
    Error: Identifier name is reserved: long at 191:23

    This error was caused by the ArrayOflong type generating the following Apex:
    public class ArrayOflong {
        public Long[] long;
        private String[] long_type_info = new String[]{'long','http://www.w3.org/2001/XMLSchema','long','0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://www.Example.com/api','false','false'};
        private String[] field_order_type_info = new String[]{'long'};
    }
    
    Possible fix:
    public class ArrayOflong {
        public Long[] long_x; // Note the _x suffix to avoid the conflict
        // The first parameter here is what will appear in the SOAP message as the element name
        private String[] long_type_info = new String[]{'long','http://www.w3.org/2001/XMLSchema','long','0','-1','false'};
        private String[] apex_schema_type_info = new String[]{'http://www.Example.com/api','false','false'};
        private String[] field_order_type_info = new String[]{'long_x'};
    }
    
  8. Error: Failed to parse wsdl: Unsupported Schema element found http://www.w3.org/2001/XMLSchema:attribute. At: 1866:57
    The WSDL had three "attribute" elements:
    • <xs:attribute name="FactoryType" type="xs:QName"/>
    • <xs:attribute name="Id" type="xs:ID"/>
    • <xs:attribute name="Ref" type="xs:IDREF"/>
    Commenting these out of the WSDL allowed into to be used with wsdl2apex. I'm not sure what adverse effects resulted from removing them.
  9. The use of <xsd:extension base="foo"> in a complex type won't result in the base class fields appearing in generated Apex class.
    Try copying the fields from Apex class that was being extended into the sub class.

See also:

ASP.NET button in Sitecore is posting back to the layout rather than the sublayout

After setting up a new development environment for an existing Sitecore application where all the custom code was retrieved from the source control repository we ran into an issue.

Problem

All buttons on sublayouts which should post back to the page where the buttons are sitting - instead go to the layout where the form element is defined.

Neil Pullinger's blog described the same issue and a solution.

Solution

We found that the new sitecore install did not have the App_Browser folder and was missing two files from that folder.

  1. Form.browser
  2. Xaml.browser

After coping files from an existing working development site to the new Sitecore project and it fixed the problem.

See also:

Thursday, March 3, 2011

APEX SOQL with an IN Clause using a map.KeySet()

I've been working on some APEX that builds up a Map where the keys are Id's. It goes through several SOQL calls adding additional items that aren't already in the map. As such it is useful to exclude the items already in the map when performing the SOQL call.

Map<Id, Account> mapAccounts = new Map();

SET<ID> keys = mapAccounts.keyset();
//...

for (List<Account> listAccounts: [select Id, Name, BillingCity, BillingCountry from Account where 
 Name like :searchTerm and
 Id NOT IN :keys ]) {
 for ( Account account : listAccounts) {
  mapAccounts.put(account.Id, account);
 }
}

See Also: