Thursday, March 10, 2011

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:

Tuesday, February 22, 2011

Using Notepad++ to make Apex Test Runner Debug output readable

When running the Apex Test Runner in Eclipse with the Log level set to Debug it can be difficult to see your debug messages through the noise of method entry and method exit messages.

Notepad++

I've been copying the output into Notepad++ and then using the RegExs ^.+METHOD_.+$ and ^.+CONSTRUCTOR_.+$ followed by[ TextFX > TextFX Edit > Delete Blank Lines] to clean up the output.

With v5.8.7 I was able to combine all this into a Macro.

Visual Studio 2010

Better yet, copy it into a Visual Studio 2010 txt file and add a \n to the end of each regex.

Eclipse

Another good option is to adjust the amount of logging that occurs in the first place. See Setting Debug Log Filters

System Log | Force.com Console | Developer Console

The online logging has come a long way since this was first posted. Assuming the log isn't too big you can now Filter the execution log directly online.

See also:

Monday, February 21, 2011

Nelson .NET User Group Presentation - Using Powershell to aide builds and deployments

Upcoming meeting

This presentation is coming up next week. Please let me know if you are planning of attending.

Title:

Using Powershell to aide builds and deployments

Abstract:

Windows PowerShell is a powerful scripting framework and command line shell provided by Microsoft, and in a windows development environment is a natural candidate to integrate with your builds and deployments. Stepping through some of the basics of source and build management in Team Foundation Server it will be shown how you can easily extend your development environment using PowerShell to automate some of the more mundane and error prone tasks.

Giveaways:

I have a copy of Windows 7 Ultimate (64-bit) to give away during the presentation.
The winner will be selected through a random draw.

Useful links:

When:
Tuesday 1st March 2011
Gather at 11:50 am, starting at 12:00 pm.
Approximately 1 hour plus pizza afterwards.

Where:
FuseIT Ltd,
Ground Floor,
7 Forests Rd,
Stoke,
Nelson
(Off Nayland Rd and behind Carters)

http://local.live.com/default.aspx?v=2&cp=-41.299774~173.236231&style=r&lvl=16&alt=-1000
or
http://maps.google.com/?ie=UTF8&om=1&z=17&ll=-41.299774,173.236231&spn=0.005239,0.010042&t=h

If you are parking on site, please use the parks marked FuseIT that are at the back of the site.

Catering: Pizza & Drinks
Door Charge: Free

RSVP to me if you are going to attend so I can guesstimate the food and drink requirements.

However, feel free to turn up on the day though if you can't commit at the moment.

Please feel free to invite anyone who may be interested in attending.

Wednesday, February 9, 2011

Removing a log4net log file with a custom uninstaller

After creating a Windows Installer Setup Project to install a basic forms application demo application testing found that the program files directory that the installer created was not removed due to a log file that was created by log4net.

To resolve the issue a Installer Class was added to the winforms project with an override for Uninstall. Adding the custom Installer class to the existing project ensures that is was included in the Application Folder for the target machines File System without bringing in another DLL.

[RunInstaller(true)]
 public partial class Installer1 : System.Configuration.Install.Installer
 {
  public Installer1()
  {
   InitializeComponent();
  }

  [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand)]
  public override void Uninstall(IDictionary savedState)
  {
   base.Uninstall(savedState);

   //This is handy if you need to attach the debugger to the custom install action.
   //System.Diagnostics.Debugger.Break();

   try
   {
    string basePath = this.Context.Parameters["assemblyPath"];

    string baseDirectory = Path.GetDirectoryName(basePath);

    string logFilePath = Path.Combine(baseDirectory, "testapp.log");
    if (File.Exists(logFilePath))
    {
     File.Delete(logFilePath);
    }
   }
   catch
   {
    //Failed to delete the log file
   }

  }

 }

Finally the custom Uninstall implementation is connected to the Windows Installer.