MTOM with Axis2 version 1.3#

A step-by-step guide to writing a web service which supports MTOM and a web service client to call it. The example assumes a Windows environment and Eclipse as the development environment.

Setup the Development Environment

1. Download Tomcat version 5.5 from http://tomcat.apache.org/download-55.cgi

2. Set the JAVA_HOME environment variable to the Java JDK directory. Note that you have to set the variable to the JDK and not the JRE install folder.

3. Run startup.bat from the Tomcat distribution bin directory to make sure Tomcat will run. If all is good, run shutdown.bat from the bin directory to stop the Tomcat server.

4. Download Axis2 version 1.3 (http://ws.apache.org/axis2/download/1_3/download.cgi)

a. Download the Standard Distribution and the WAR distribution.

5. Copy the axis2.war file from the WAR distribution in step 4 to the webapps folder under Tomcat. Start Tomcat by running startup.bat. If Tomcat was able to consume the war file, then you should be able to browse to http://localhost:8080/axis2/ to see the Axis2 web application default page.

6. Upon startup, Tomcat unpacked the axis2.war file. Have a look at the axis2.xml configuration file within axis2\WEB-INF\conf\ folder.

7. The axis2.xml file contains the administrator username and password for axis2 administration. By default the username and password to admin and axis2, respectively. You can modify this by changing the values and restarting Tomcat.

8. Point your browser to the axis2 web application and choose the Administration link. http://localhost:8080/axis2/axis2-admin/login. Type in the username and password to access the admin console.

9. So far we have Tomcat and Axis2 up and running. To make this exercise less painful, we should also download TCPMon so we can view the SOAP message we create. Download TCPMon from: http://ws.apache.org/commons/tcpmon/download.cgi. This utility is very cool and easy to use. Before you start TCPMon (by running tcpmon.bat from the build), have a look at the user guide. http://wso2.org/project/wsas/java/2.1/docs/tools/tcpmonguide.html

Create MTOM Enabled Web Service

1. Create a new Java Project in Eclipse. Name the project TestMTOM.

2. Add the axis2 jar files to the classpath. The axis2 jar files are within the lib directory of the axis2 standard distribution.

3. Create a new class named TestService within a package named com.test. Paste the following for the class implementation.

package com.test;

import java.io.File;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import javax.activation.DataHandler;

import org.apache.axiom.om.OMElement;

import org.apache.axiom.om.OMText;

public class TestService

{

private static final String OUTPUT_FILE = "C:\\HOLD\\att.pdf";

public void receiveMTOM(OMElement element) throws Exception

{

System.out.println("received request...");

OMText binaryNode = (OMText) (element.getFirstElement()).getFirstOMChild();

binaryNode.setOptimize(true);

DataHandler dh = (DataHandler) binaryNode.getDataHandler();

InputStream is = dh.getInputStream();

byte[] buf = readFully(is);

writeOutput(buf);

System.out.println("done writing output file.");

}

private static void writeOutput(byte[] buf)throws IOException

{

File of = new File(OUTPUT_FILE);

if(of.exists())

{

of.delete();

}

of.createNewFile();

setContents(new File(OUTPUT_FILE),buf);

}

private static void setContents(File aFile, byte[] aContents) throws FileNotFoundException, IOException

{

if (aFile == null)

{

throw new IllegalArgumentException("File should not be null.");

}

if (!aFile.exists())

{

throw new FileNotFoundException("File does not exist: " + aFile);

}

if (!aFile.isFile())

{

throw new IllegalArgumentException("Should not be a directory: " + aFile);

}

if (!aFile.canWrite())

{

throw new IllegalArgumentException("File cannot be written: " + aFile);

}

/**

* declared here only to make visible to finally clause;

* generic reference

*/

OutputStream output = null;

try

{

output = new FileOutputStream(aFile);

output.write(aContents);

}

finally

{

/**

* flush and close both "output" and its underlying FileWriter

*/

if (output != null)

output.close();

}

}

private static byte[] readFully(InputStream is) throws IOException

{

int size = 10000;

/**

* Offset - how much we've read

*/

int off = 0;

int got;

byte[] ret = new byte[size];

try

{

while (true)

{

got = is.read(ret, off, size - off);

/**

* End of stream

*/

if (got == -1)

break;

off += got;

if (off == size)

{

/**

* If we've read to the end of our buffer,

* enlarge it.

*/

size *= 2;

byte[] tmp = new byte[size];

System.arraycopy(ret, 0, tmp, 0, off);

ret = tmp;

}

}

}

finally

{

if(is!=null)is.close();

}

/**

* If we've got a bigger buffer than we need, resize it

*/

if (off != size)

{

byte[] tmp = new byte[off];

System.arraycopy(ret, 0, tmp, 0, off);

ret = tmp;

}

return ret;

}

}

The receiveMTOM() method expects an OMElement, which then contains the binary file. The method simply pulls the binary file out of the SOAP message and saves it to the local file system. The file is identified by the OUTPUT_FILE static variable. Note that we have to

binaryNode.setOptimize(true);

prior to calling the getDataHandler() method.

4. The service implementation is done. Now we have to create a service.xml file. To do that, first create a folder under the root of the project and name it META-INF. Within this folder, create an xml file and name it services.xml. Paste the following into that file.

<service name="TestService">

<description>

This service is to get the running Axis version

</description>

<parameter name="ServiceClass">com.test.TestService</parameter>

<parameter name="enableMTOM">true</parameter>

<operation name="receiveMTOM">

<messageReceiver class="org.apache.axis2.rpc.receivers.RPCMessageReceiver" />

</operation>

</service>

To enable MTOM, we have to add a parameter for the service called enableMTOM and set its value to true. Note that MTOM is disabled by default.

5. The next thing we have to do is package the service as a jar file but with a "aar" extension. You can do this several ways, but the easiest approach is to just use Eclipse's Export functionality.

6. Once you have the aar file, copy it under the axis2 deployment within Tomcat. This would be at TOMCAT_INSTALL\webapps\axis2\WEB-INF\services\

7. Restart Tomcat.

Create MTOM enabled Web Service Client

1. Before we can call the service, we need a proxy to the web service. Assuming Tomcat is running, browse to the following URL. http://localhost:8080/axis2/services/TestService?wsdl

2. To create the proxy, save the WSDL file to the bin directory of the Axis2 installation. Name the file TestService.wsdl. Note that within this directory, you'll also have the WSDL2Java.bat file that we need to use to create the web service proxy.

3. After you save the WSDL file, open a command prompt and execute

WSDL2JAVA -uri TestService.wsdl

This should create the proxy files with a folder name src.

4. Now we need to create a java project within Eclipse for the web service client. Create a new Project and name it TestMTOMClient.

5. Add the Axis2 jar files to the classpath.

6. Copy the src folder from step 3 under the TestMTOMClient project and refresh the project in Eclipse.

7. Now create a class to execute the web service. Name the class TestClient and paste the following in its place.

package com.test;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

import junit.framework.TestCase;

import org.apache.axiom.om.OMAbstractFactory;

import org.apache.axiom.om.OMElement;

import org.apache.axiom.om.OMFactory;

import org.apache.axiom.om.OMNamespace;

import org.apache.axiom.om.OMText;

import org.apache.axis2.Constants;

import org.apache.axis2.util.Base64;

import com.test.TestServiceStub.ReceiveMTOM;

 

public class TestClient extends TestCase

{

private static final String EPR = "http://localhost:9080/axis2/services/TestService";

private static final String INPUT_FILE = "C:\\HOLD\\small.pdf";

public static void main(String[] args) throws Exception

{

}

public void testMTOM()throws Exception

{

TestServiceStub stub = new TestServiceStub(EPR);

stub._getServiceClient().getOptions().setProperty(

Constants.Configuration.ENABLE_MTOM, Constants.VALUE_TRUE);

/**

* set timeout

*/

stub._getServiceClient().getOptions().setTimeOutInMilliSeconds(10000);

String base64String = Base64.encode(readFully(INPUT_FILE));

ReceiveMTOM rmtom = new ReceiveMTOM();

OMFactory fac = OMAbstractFactory.getOMFactory();

OMText binaryNode =fac.createOMText(base64String,"application/pdf",true);

OMNamespace omNs = fac.createOMNamespace("http://test.com", "ns0");

OMElement method = fac.createOMElement("receiveMTOM", omNs);

method.addChild(binaryNode);

rmtom.setElement(method);

stub.receiveMTOM(rmtom);

System.out.println("done calling service...");

}

}

The testMTOM() method creates a stub and sets the enableMTOM property. It then reads the a file and then does a base64 encoded on the file's contents. Finally, it calls the receiveMTOM() metod on the stub. Note that to call the web service, we need a sample file. The class above assumes there is a file named "small.pdf" at c:\hold\. So create a folder named hold in the root of the c:\ drive and copy a pdf file in that directory. Name the file small.pdf.

9. Before you call the service, start TCPMon so we can view the SOAP message that the client sends to the service so you can verify that the SOAP Message is really an MTOM enabled SOAP request. Configurre TCPMon so that the listen port is 9080 and the listener's Target port is 8080. Assuming that Tomcat is running on port 8080. Also realize that the client above is setup to call the service on 9080 so as long as we configure TCPMon correctly we should be okay. If you are not using TCPMon, change the EPR variable in the client class so that it points to port 8080 or whatever port your service is running on.

10. Assuming you did everything correctly, you should be able to send some MTOM SOAP messages to the service. When the service receives a request, it save the binary file to c:\hold\att.pdf.

 

References

1. Download Tomcat: http://tomcat.apache.org/download-55.cgi

2. Download Axis2 version 1.3: http://ws.apache.org/axis2/download/1_3/download.cgi

3. Download TCPMonitor: http://ws.apache.org/commons/tcpmon/download.cgi

4. Using TCPMonitor: http://wso2.org/project/wsas/java/2.1/docs/tools/tcpmonguide.html

5. A very good article on MTOM with Axis2: http://ws.apache.org/axis2/1_1_1/mtom-guide.html

6. Apache MTOM User Guide: http://ws.apache.org/axis2/1_0/mtom-guide.html

11/9/2007 3:22:14 PM (Eastern Standard Time, UTC-05:00) #    Comments [4]  |  Trackback

 

WS-Security, Axis2 and Rampart#

One of the things that I don’t like about Java is that the community is not using the same foundational libraries. For example, there is not a standard J2EE application server. Instead there is a specification and hundreds of vendors. As a result, everyone seems to be using a different implementation of a specification, and thus finding answers to questions is a lot harder because the user community is spread across many many implementations.

Recently, I had to call a web service which was protected by WS-Security UsernameToken. I was able to quickly call the service with Axis 1.1 and WSS4J but when I tried to use Axis2 and Rampart I ran into exception after exception. After several days of struggling, I was eventually able to create a web service client that created the proper soap message.

This blog contains a step by step procedure to create a web service client to a web service which is protected by ws-security UsernameToken.

I am assuming that you have already deployed the web service which is protected by ws-security and that you simply want to call the service using Axis2 version 1.3 and Rampart 1.3. Here are the steps:

1) Download Axis2 version 1.3 from http://ws.apache.org/axis2/

2) Download Rampart 1.3 from http://ws.apache.org/axis2/modules/index.html

3) Add Axis2 JARs to your client application classpath--all JARs from the Axis2 distribution lib directory.

3) Add the Rampart jars to your client application classpath--all jars from the Rampart distribution lib directory.

4) Create a proxy to the web service that is protected by ws-security--using the WSDL2Java tool or batch file.

5) Create an Axis2 client-configuration file which engages rampart and which has the ws-security parameter. Place this file within your client application project. For example, the standard is to create a folder called "conf" for this file.

Here is an example file:

 

<axisconfig name="AxisJava2.0">

<!-- engage rampart -->

<module ref="rampart" />

 

<!-- ws-security parameters: UsernameToken and PasswordText -->

<parameter name="OutflowSecurity">

<action>

<items>UsernameToken</items>

<user>myusername</user>

<passwordCallbackClass>com.mycomp.test.PWCBHandler</passwordCallbackClass>

<passwordType>PasswordText</passwordType>

</action>

</parameter>

<!-- ================================================= -->

<!-- Parameters -->

<!-- ================================================= -->

<parameter name="hotdeployment">true</parameter>

<parameter name="hotupdate">false</parameter>

<parameter name="enableMTOM">false</parameter>

<parameter name="enableSwA">false</parameter>

<!--Uncomment if you want to enable file caching for attachments -->

<!--parameter name="cacheAttachments">true</parameter>

<parameter name="attachmentDIR"></parameter>

<parameter name="sizeThreshold">4000</parameter-->

<!--This will give out the timout of the configuration contexts, in seconds-->

<parameter name="ConfigContextTimeoutInterval">30</parameter>

<!--During a fault, stacktrace can be sent with the fault message. The following flag will control -->

<!--that behaviour.-->

<parameter name="sendStacktraceDetailsWithFaults">false</parameter>

<parameter name="DrillDownToRootCauseForFaultReason">false</parameter>

<parameter name="userName">admin</parameter>

<parameter name="password">axis2</parameter>

<!--Set the flag to true if you want to enable transport level session mangment-->

<parameter name="manageTransportSession">false</parameter>

<!-- Following parameter will completely disable REST handling in Axis2-->

<parameter name="disableREST" locked="true">false</parameter>

<!-- ================================================= -->

<!-- Message Receivers -->

<!-- ================================================= -->

<!--This is the Deafult Message Receiver for the system , if you want to have MessageReceivers for -->

<!--all the other MEP implement it and add the correct entry to here , so that you can refer from-->

<!--any operation -->

<!--Note : You can ovride this for particular service by adding the same element with your requirement-->

<messageReceivers>

<messageReceiver mep="http://www.w3.org/2004/08/wsdl/in-only"

class="org.apache.axis2.receivers.RawXMLINOnlyMessageReceiver"/>

<messageReceiver mep="http://www.w3.org/2004/08/wsdl/in-out"

class="org.apache.axis2.receivers.RawXMLINOutMessageReceiver"/>

</messageReceivers>

<!-- ================================================= -->

<!-- Message Formatter -->

<!-- ================================================= -->

<!--Following content type to message formatter mapping can be used to implement support for different message -->

<!--format serialization in Axis2. These message formats are expected to be resolved based on the content type. -->

<messageFormatters>

<messageFormatter contentType="application/x-www-form-urlencoded"

class="org.apache.axis2.transport.http.XFormURLEncodedFormatter"/>

<messageFormatter contentType="application/xml"

class="org.apache.axis2.transport.http.ApplicationXMLFormatter"/>

<messageFormatter contentType="text/xml"

class="org.apache.axis2.transport.http.ApplicationXMLFormatter"/>

<messageFormatter contentType="application/echo+xml"

class="org.apache.axis2.transport.http.ApplicationXMLFormatter"/>

</messageFormatters>

<!-- ================================================= -->

<!-- Transport Ins -->

<!-- ================================================= -->

<transportReceiver name="http"

class="org.apache.axis2.transport.http.SimpleHTTPServer">

<parameter name="port">6060</parameter>

</transportReceiver>

<!--Uncomment this and configure as appropriate for JMS transport support, after setting up your JMS environment (e.g. ActiveMQ)

<transportReceiver name="jms" class="org.apache.axis2.transport.jms.JMSListener">

<parameter name="myTopicConnectionFactory">

<parameter name="java.naming.factory.initial">org.apache.activemq.jndi.ActiveMQInitialContextFactory</parameter>

<parameter name="java.naming.provider.url">tcp://localhost:61616</parameter>

<parameter name="transport.jms.ConnectionFactoryJNDIName">TopicConnectionFactory</parameter>

</parameter>

<parameter name="myQueueConnectionFactory">

<parameter name="java.naming.factory.initial">org.apache.activemq.jndi.ActiveMQInitialContextFactory</parameter>

<parameter name="java.naming.provider.url">tcp://localhost:61616</parameter>

<parameter name="transport.jms.ConnectionFactoryJNDIName">QueueConnectionFactory</parameter>

</parameter>

<parameter name="default">

<parameter name="java.naming.factory.initial">org.apache.activemq.jndi.ActiveMQInitialContextFactory</parameter>

<parameter name="java.naming.provider.url">tcp://localhost:61616</parameter>

<parameter name="transport.jms.ConnectionFactoryJNDIName">QueueConnectionFactory</parameter>

</parameter>

</transportReceiver>-->

<!-- ================================================= -->

<!-- Transport Outs -->

<!-- ================================================= -->

<transportSender name="tcp"

class="org.apache.axis2.transport.tcp.TCPTransportSender"/>

<transportSender name="local"

class="org.apache.axis2.transport.local.LocalTransportSender"/>

<transportSender name="http"

class="org.apache.axis2.transport.http.CommonsHTTPTransportSender">

<parameter name="PROTOCOL">HTTP/1.1</parameter>

<parameter name="Transfer-Encoding">chunked</parameter>

<!-- If following is set to 'true', optional action part of the Content-Type will not be added to the SOAP 1.2 messages -->

<!-- <parameter name="OmitSOAP12Action">true</parameter> -->

</transportSender>

<transportSender name="https"

class="org.apache.axis2.transport.http.CommonsHTTPTransportSender">

<parameter name="PROTOCOL">HTTP/1.1</parameter>

<parameter name="Transfer-Encoding">chunked</parameter>

</transportSender>

<!-- ================================================= -->

<!-- Global Modules -->

<!-- ================================================= -->

<!-- Comment this to disable Addressing -->

<module ref="addressing"/>

<!--Configuring module , providing parameters for modules whether they refer or not-->

<!--<moduleConfig name="addressing">-->

<!--<parameter name="addressingPara">N/A</parameter>-->

<!--</moduleConfig>-->

<!-- ================================================= -->

<!-- Clustering -->

<!-- ================================================= -->

<!-- Configure and uncomment following for preparing Axis2 to a clustered environment -->

<!--

<cluster class="org.apache.axis2.cluster.tribes.TribesClusterManager">

<parameter name="param1">value1</parameter>

</cluster>

-->

<!-- ================================================= -->

<!-- Phases -->

<!-- ================================================= -->

<phaseOrder type="InFlow">

<!-- System predefined phases -->

<phase name="Transport">

<handler name="RequestURIBasedDispatcher"

class="org.apache.axis2.dispatchers.RequestURIBasedDispatcher">

<order phase="Transport"/>

</handler>

<handler name="SOAPActionBasedDispatcher"

class="org.apache.axis2.dispatchers.SOAPActionBasedDispatcher">

<order phase="Transport"/>

</handler>

</phase>

<phase name="Addressing">

<handler name="AddressingBasedDispatcher"

class="org.apache.axis2.dispatchers.AddressingBasedDispatcher">

<order phase="Addressing"/>

</handler>

</phase>

<phase name="Security"/>

<phase name="PreDispatch"/>

<phase name="Dispatch" class="org.apache.axis2.engine.DispatchPhase">

<handler name="RequestURIBasedDispatcher"

class="org.apache.axis2.dispatchers.RequestURIBasedDispatcher"/>

<handler name="SOAPActionBasedDispatcher"

class="org.apache.axis2.dispatchers.SOAPActionBasedDispatcher"/>

<handler name="RequestURIOperationDispatcher"

class="org.apache.axis2.dispatchers.RequestURIOperationDispatcher"/>

<handler name="SOAPMessageBodyBasedDispatcher"

class="org.apache.axis2.dispatchers.SOAPMessageBodyBasedDispatcher"/>

<handler name="HTTPLocationBasedDispatcher"

class="org.apache.axis2.dispatchers.HTTPLocationBasedDispatcher"/>

</phase>

<phase name="RMPhase"/>

<!-- System predefined phases -->

<!-- After Postdispatch phase module author or service author can add any phase he want -->

<phase name="OperationInPhase"/>

<phase name="soapmonitorPhase"/>

</phaseOrder>

<phaseOrder type="OutFlow">

<!-- user can add his own phases to this area -->

<phase name="soapmonitorPhase"/>

<phase name="OperationOutPhase"/>

<!--system predefined phase-->

<!--these phase will run irrespective of the service-->

<phase name="RMPhase"/>

<phase name="PolicyDetermination"/>

<phase name="MessageOut"/>

<phase name="Security"/>

</phaseOrder>

<phaseOrder type="InFaultFlow">

<phase name="Addressing">

<handler name="AddressingBasedDispatcher"

class="org.apache.axis2.dispatchers.AddressingBasedDispatcher">

<order phase="Addressing"/>

</handler>

</phase>

<phase name="Security"/>

<phase name="PreDispatch"/>

<phase name="Dispatch" class="org.apache.axis2.engine.DispatchPhase">

<handler name="RequestURIBasedDispatcher"

class="org.apache.axis2.dispatchers.RequestURIBasedDispatcher"/>

<handler name="SOAPActionBasedDispatcher"

class="org.apache.axis2.dispatchers.SOAPActionBasedDispatcher"/>

<handler name="RequestURIOperationDispatcher"

class="org.apache.axis2.dispatchers.RequestURIOperationDispatcher"/>

<handler name="SOAPMessageBodyBasedDispatcher"

class="org.apache.axis2.dispatchers.SOAPMessageBodyBasedDispatcher"/>

<handler name="HTTPLocationBasedDispatcher"

class="org.apache.axis2.dispatchers.HTTPLocationBasedDispatcher"/>

</phase>

<phase name="RMPhase"/>

<!-- user can add his own phases to this area -->

<phase name="OperationInFaultPhase"/>

<phase name="soapmonitorPhase"/>

</phaseOrder>

<phaseOrder type="OutFaultFlow">

<!-- user can add his own phases to this area -->

<phase name="soapmonitorPhase"/>

<phase name="OperationOutFaultPhase"/>

<phase name="RMPhase"/>

<phase name="PolicyDetermination"/>

<phase name="MessageOut"/>

</phaseOrder>

</axisconfig>

 

The configuration file above is a typical client config file with the rampart and ws-security configuration.

6) When calling the web service through the web service stub, you'll have to give Axis the path to the Axis configuration file above. Moreover, for Axis to engage Rampart, you'll have to give Axis the path to the Axis respository folder. The repository folder needs to have a subdirectory called "modules" and within that directory, the Rampart 1.3 "mar" file has to exist. Therefore, create a folder somewhere within your project called "repository". Then create a subdirectory called "modules". Finally, copy the "rampart-1.3.mar" file from the Rampart distributable.

7) Now you are ready to make a service call. First create an instance of a custom configuration passing in the repository path and the axis configuration file path.

ConfigurationContext cxt =

ConfigurationContextFactory.createConfigurationContextFromFileSystem("c:\\folderpath\\repository","c:\\path_to_axis_config_file.xml");

8) Create a stub with the custom axis configuration instance.

HelloServiceStub stub = new HelloServiceStub(ctx,"http://localhost:8080/services/HelloService");

HelloServiceStub.Echo echoReq = new HelloServiceStub.Echo();

System.out.println("calling service...");

HelloServiceStub.EchoResponse resp = stub.echo(echoReq);

System.out.println("done calling service...");

The key to the whole process was ConfigurationContextFactory.createConfigurationContextFromFileSystem. When calling this method, you need to make sure you pass the correct repository path and a proper Axis2 client configuration file. The path to the repository has to have a sub directory called "modules" and that has to contain the rampart "mar" file. If this is not correct, then Axis2 will not be able to engage Rampart.

10/9/2007 8:00:29 AM (Eastern Daylight Time, UTC-04:00) #    Comments [4]  |  Trackback

 

How to add a start menu icon for your ClickOnce deployed application using MSBuild?#
If you have an automated build and deployment process using MSBuild, then you likely use the GenerateApplicationManifest and GenerateDeploymentManifest tasks to create the ClickOnce application and deployment manifest, respectively. If you are using these tasks, you may have wondered how you can set an icon (under the start menu) for your application. To have an icon set, you have to do several things:
 
1) Package the icon with the deployment. The easiest way to do this is to add an icon file to your main project--that ensure that it gets deployed with the application.
2) After you add the icon file, right click on the file in Visual Studio, and set the Build Action to Content.
3) At deployment time, set the IconFile attribute on GenerateApplicationManifest to the icon file name (e.g., myapp.ico), you added in step (1).
 
The above will add an iconFile attribute to the assembly description element, e.g., <description iconFile="myapp.ico" />, in the application manifest file (e.g., myapp.exe.manifest). When ClickOnce sees this attribute set, it will look in the ClickOnce deployment for the file and create a Start Menu Icon for your application.
 
An interesting thing to note is that the iconFile attribute is set in the application manifest and not in the deployment manifest. The reason for this is to allow you to change your application icons from one deployment to the next.
 
 
If you are deploying your ClickOnce application using Visual Studio and need to set the Start Mneu icon, do steps (1) and (2), and then go to the Project | Properties and choose the Application tab. Under the Resources group box, set the application icon to the icon you added in step (1). ClickOnce should take care of the rest. You can find out more on this at http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=170920&SiteID=1 .
10/11/2006 9:20:56 AM (Eastern Daylight Time, UTC-04:00) #    Comments [4]  |  Trackback

 

Application Block Internals: Demystifying the Object Builder#

Demystifying the ObjectBuilder

 

In my last post, I gave an introduction to ObjectBuilder. In this post, I take ObjectBuilder apart and talk about all of the hairy details.

 

ObjectBuilder is used in the Enterprise Library, the CAB, and the Mobile framework MS just released. If you have to extend any of the above frameworks/libraries in a non-trivial way, you are going to need to understand what is going on under the covers. Under the covers of the above frameworks/libraries is ObjectBuilder--the dependency injection framework.

 

Unfortunately, there is not any documentation for the ObjectBuilder, so learning is a real nightmare. I was able to break it by going over the unittests that ship with the source code.

 

Fundamental Classes in Object Builder

 

Strategy and Policies - OB is based upon policies and strategies. Strategies are chained (i.e. OB implements the Chain of Responsability pattern on strategies) and get registered for a build stage.  Strategies use policies to figure out how to build an object. Policies are registered with OB for types. A policy is defined for types (objects in OB are defined by the type and ID).

Locator - locators in OB are used to find registered objects. When an object is created, it gets registered with the locator (see CreationStrategy.RegisterObject).

LifetimeContainer - objects managed by the object builder can have a lifetime associated with them. The thing that determines how long an object stays around is the container that the object is associated with. LifetimeContainers in OB, maintain a list of objects. When the container is disposed off, the objects in it are also disposed.

BuilderContext- an object that defines the context for the build-up and tear-down of an object. BuilderContext holds the strategies, policies and locator for the given build-up or tear-down. It also provides a method to iterate the chain of strategies (see IBuilderContext and BuilderContext).

 

OB By Example

 

/**********Create A Singleton*************/

public void CreateASingleton()
{
    // we need a locator, a strategy chain, and a list of policies.
    Locator locator = new Locator();
    BuilderStrategyChain strategyChain = new BuilderStrategyChain();
    PolicyList policies = new PolicyList();
    // in order to build a singleton, we have to
    // have a SingletonStrategy. The singleton strategy
    // in turn uses a SingletonPolicy.
    // add a SingletonStrategy to the strategy chain
    strategyChain.Add(new SingletonStrategy());
    // add a CreationStrategy to the strategy chain
    strategyChain.Add(new CreationStrategy());
    // SingletonStrategy requires a SingletonPolicy
    policies.Set<ISingletonPolicy>(new SingletonPolicy(true), typeof(MyObject), null);
    // we also need a creation policy
    policies.SetDefault<ICreationPolicy>(new DefaultCreationPolicy());
    // in order to make singletons, we need a lifetime container in the locator
    locator.Add(typeof(ILifetimeContainer), new LifetimeContainer());
    // create the object
    BuilderContext cxt = new BuilderContext(strategyChain, locator, policies);
    // in order to properly track singletons, we have to give the instance an ID.
    object myObj = strategyChain.Head.BuildUp(cxt, typeof(MyObject), "MyObject_Singleton", null);
    object myObj2 = strategyChain.Head.BuildUp(cxt, typeof(MyObject), "MyObject_Singleton", null);
    if (myObj == myObj2)
    {
        // Got singleton in myObj2
        int J = 0;
    }
}

 

The example above demonstrates creating singleton objects with the OB. Most of the work simply sets up using the OB. For example, in order to create an object, we need to have a build context. A build context requires a locator, a strategy chain, and a policy list. There are several pieces that enable singletons, however. For example, utimately a strategy is the thing that will create an object in OB. Strategies rely on policies to determine how to create an object. To create a singleton, we have to have a SingletonStrategy in the strategy chain. The singleton strategy looks for a SingletonPolicy, registered for the object being created. Recall that policies are setup for types.

 

There are a few design aspects of OB that we have to understand. OB uses something called a Locator. A locator knows how to find registered objects. Locators can be nested (i.e., a locator can have a parent). Locators make use of something called a LifetimeContainer. LifetimeContainer puts a boundary around the lifetime of a created object. When a locator is asked to find an object it can look in the current locator and/or it's parent locator (if one exists).

 

It's important to know that when searching for singletons, OB looks only at the current locator--the parent is not searched. The SingletonStrategy class in OB is shown below for reinforcement of this.

 

public class SingletonStrategy : BuilderStrategy
 {
    public override object BuildUp(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
    {

       DependencyResolutionLocatorKey key = new DependencyResolutionLocatorKey(typeToBuild, idToBuild);

 

       if (context.Locator != null && context.Locator.Contains(key, SearchMode.Local))
       {
            TraceBuildUp(context, typeToBuild, idToBuild, "");
             return context.Locator.Get(key);
       }

 

       return base.BuildUp(context, typeToBuild, existing, idToBuild);
    }
 }

 

As shown, the SingletonStrategy checks to see if the build context has a locator,and then asks the locator to find the object using a SearchMode.Local. This tells the locator not to look in the parent locator for the object. This has obvious usage implications--if you don't create your singletons with the correct locator, then you'll end up breaking the singleton (TODO: more on this.).

 

Also note that we have put a CreationStrategy in the chain of strategies for our singleton object. The SingletonStrategy ensures that once we have an object, that object is returned on subseqent build-up requests. The CreationStrategy is needed to build-up the object the first time. Since the CreationStrategy is the strategy that builds the object, this strategy also registers the object with the locator/container so that it can be pulled out the next time. We can see this by looking at the CreationStrategy.

 

public class CreationStrategy : BuilderStrategy
{
  public override object BuildUp(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
  {
   if (existing != null)
    BuildUpExistingObject(context, typeToBuild, existing, idToBuild);
   else
    existing = BuildUpNewObject(context, typeToBuild, existing, idToBuild);

 

   return base.BuildUp(context, typeToBuild, existing, idToBuild);
  }

 

  private void BuildUpExistingObject(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
  {
   RegisterObject(context, typeToBuild, existing, idToBuild);
  }

 

  [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.SerializationFormatter)]
  private object BuildUpNewObject(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
  {
   ICreationPolicy policy = context.Policies.Get<ICreationPolicy>(typeToBuild, idToBuild);

 

   if (policy == null)
   {
    if (idToBuild == null)
     throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
      Properties.Resources.MissingPolicyUnnamed, typeToBuild));
    else
     throw new ArgumentException(String.Format(CultureInfo.CurrentCulture,
      Properties.Resources.MissingPolicyNamed, typeToBuild, idToBuild));
   }

 

   try
   {
    existing = FormatterServices.GetSafeUninitializedObject(typeToBuild);
   }
   catch (MemberAccessException exception)
   {
    throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Properties.Resources.CannotCreateInstanceOfType, typeToBuild), exception);
   }

 

   RegisterObject(context, typeToBuild, existing, idToBuild);
   InitializeObject(context, existing, idToBuild, policy);
   return existing;
  }

 

  private void RegisterObject(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
  {
   if (context.Locator != null)
   {
    ILifetimeContainer lifetime = context.Locator.Get<ILifetimeContainer>(typeof(ILifetimeContainer), SearchMode.Local);

 

    if (lifetime != null)
    {
     ISingletonPolicy singletonPolicy = context.Policies.Get<ISingletonPolicy>(typeToBuild, idToBuild);

 

     if (singletonPolicy != null && singletonPolicy.IsSingleton)
     {
      context.Locator.Add(new DependencyResolutionLocatorKey(typeToBuild, idToBuild), existing);
      lifetime.Add(existing);

 

      if (TraceEnabled(context))
       TraceBuildUp(context, typeToBuild, idToBuild, Properties.Resources.SingletonRegistered);
     }
    }
   }
  }
  //...more methods ....
}

 

As shown above, the BuildUp method checks to see if it has to build-up a new object or an existing one. In our case, we are building up an object for the firsttime. In this case, the BuildUpNewObject method is called. This method looks on the build context for a ICreationPolicy for the required object. If it doesn't find one, it throws an exception--in other words, you have to have a policy registered for the object that tells OB how to create the object. In our case, we are using the DefaultCreationPolicy. If there is a ICreationPolicy, then the method builds a bare-bones object (one that is not initialized) using 

 

existing = FormatterServices.GetSafeUninitializedObject(typeToBuild);

 

and then calls RegisterObject(). This is where the singleton magic happens. As you can see from the code snippet above, the CreationStrategy looks for a lifetime container on the local locator and if one exists, it looks for a singleton policy registered for the required object. If it finds the singleton policy, it adds a key for the newly build object to the lifetime container.

 

Thus, we can conclude that in order to create singleton objects with OB, we need to have a SingletonStrategy and a SingletonPolicy. There are two aspects to creating a singleton:

 

  1. When creating an object, you have to register it.
  2. When creating an object, you have to check to see if one already exists.

The singleton policy, ensures that the object gets registered with the local locator's lifetime container and the singleton strategy ensures that the registered object gets returned on subsequent build-up operations.

 

OB is a glorified object factory and DI framework. To support DI, OB ships with some strategies and policies. Lets investigate these now. [TODO: Correct this intro into DI via Properties]

 

Injecting into Properties

 

DI in OB is achived at build-up. When you ask OB to build-up an object, OB runs a chain of strategies. The idea is to inject dependencies into objects at build-up. To do that, you put a strategy object, an object that knows how to do the proper injection, into the chain of strategies. One of the strategies that knows how to inject dependencies is the PropertySetterStrategy. An example will help.

 

public void PropertyInjectionExample()
{
 Locator locator = new Locator();
 LifetimeContainer container = new LifetimeContainer();
 locator.Add(typeof(ILifetimeContainer), container);
 // strategies ...
 BuilderStrategyChain chain = new BuilderStrategyChain();
 chain.Add(new CreationStrategy());
 chain.Add(new PropertySetterStrategy());
 // policies...
 PolicyList policies = new PolicyList();
 // Property setter policy for MyDAOObject's ConnectionString
 PropertySetterPolicy psp = new PropertySetterPolicy();
 // add a property for the ConnectionString property
 psp.Properties.Add("ConnectionString", new PropertySetterInfo("ConnectionString",new ValueParameter<string>("the connection string value would be here")));
 policies.Set<IPropertySetterPolicy>(psp, typeof(MyDAOObject), null);
 policies.SetDefault<ICreationPolicy>(new DefaultCreationPolicy());
 // create build conetxt...
 BuilderContext cxt = new BuilderContext(chain, locator, policies);
 // build the object
 MyDAOObject obj = chain.Head.BuildUp(cxt, typeof(MyDAOObject), null, null) as MyDAOObject;
}

 

class MyDAOObject
{
    private string connectionString;
    public string ConnectionString
    {
        get
        {
            return connectionString;
        }
        set
        {
            connectionString = value;
        }
    }
    public MyDAOObject():this(null) { }
    public MyDAOObject(string conStr)
    {
        connectionString = conStr;
    }
}

 

In our introduction, we mentioned that DI is achieved by injecting dependencies into properties or via constructors. The PropertySetterStrategy, as the name suggests, injects dependencies using properties. In order to get properties injected into your objects, you have to add a PropertySetterStrategy instance to the strategy chain (as shown above). The BuildUp method of this strategy is shown below.

 

public override object BuildUp(IBuilderContext context, Type typeToBuild, object existing, string idToBuild)
{
 if (existing != null)
  InjectProperties(context, existing, idToBuild);

 

 return base.BuildUp(context, typeToBuild, existing, idToBuild);
}

 

As you can see, the BuildUp method ensures that the object has been created and then it calls the InjectProperties method. Note the implication here--you have to make sure you have a strategy that creates the object ahead of the PropertySetterStrategy in the strategy chain. Otherwise, the strategy is effectively skipped because the base class's BuildUp is called to call the next strategy in the chain. Now lets have a look at the InjectProperties method (see below).

 

private void InjectProperties(IBuilderContext context, object obj, string id)
{
 if (obj == null)
  return;

 

 Type type = obj.GetType();
 IPropertySetterPolicy policy = context.Policies.Get<IPropertySetterPolicy>(type, id);

 

 if (policy == null)
  return;

 

 foreach (IPropertySetterInfo propSetterInfo in policy.Properties.Values)
 {
  PropertyInfo propInfo = propSetterInfo.SelectProperty(context, type, id);

 

  if (propInfo != null)
  {
   if (propInfo.CanWrite)
   {
    object value = propSetterInfo.GetValue(context, type, id, propInfo);

 

    if( value != null )
     Guard.TypeIsAssignableFromType(propInfo.PropertyType, value.GetType(), obj.GetType());

 

    if (TraceEnabled(context))
     TraceBuildUp(context, type, id, Properties.Resources.CallingProperty, propInfo.Name, propInfo.PropertyType.Name);

 

    propInfo.SetValue(obj, value, null);
   }
   else
   {
    throw new ArgumentException(String.Format(
     CultureInfo.CurrentCulture,
     Properties.Resources.CannotInjectReadOnlyProperty,
     type, propInfo.Name));
   }
  }
 }
}

 

The InjectProperties method checks to see if an object exists and then looks for an implementation of IPropertySetterPolicy registered for the type. If it finds an IPropertySetterPolicy for the type, it iterates over the list properties that need to be injected. The list of properties that need to be injected and the values that need to be injected into these properties are defined by IPropertySetterPolicy.

 

public interface IPropertySetterPolicy : IBuilderPolicy
{
 Dictionary<string, IPropertySetterInfo> Properties { get; }
}

 

Every property that needs to be injected has to have an entry into this dictionary. Every property has a string key and an IPropertySetterInfo instance that defines the property and the value for that property. Therefore, in our example above, when we create the PropertySetterPolicy for MyObject, we added the following for the ConnectionString property.

 

psp.Properties.Add("ConnectionString", new PropertySetterInfo("ConnectionString",new ValueParameter<string>("the connection string value would be here")));

 

The Add method adds an entry with the key "ConnectionString" and a new PropertySetterInfo for the ConnectionString property. If we go back to the InjectProperties method, we can see that the method iterates over the list of IPropertySetterInfo objects. For each property configured for injection, it calls the SelectProperty method on the IPropertySetterInfo object. The default implementation of this interface (PropertySetterInfo) looks at the type to see if it has a property with given name and if it finds one, it returns a PropertyInfo for that type. Note that when we created the PropertySetterInfo object for the property, we gave it the name of the property along with the ValueParameter instance in the constructor of PropertySetterInfo. The IPropertySetterInfo interface defines the SelectProperty method, mentioned above, and the GetValue method (see below). The GetValue method returns the value that has to be injected into the property.

 

public interface IPropertySetterInfo
{
 object GetValue(IBuilderContext context, Type type, string id, PropertyInfo propInfo);

 

 PropertyInfo SelectProperty(IBuilderContext context, Type type, string id);
}

 

After the InjectProperty method gets the PropertyInfo object for the property, it checks to see if the property can be written to (i.e., if it has a setter), and then calls the GetValue method on the IPropertySetterInfo implementation. The default implementation of this interface calls the GetValue method on the IParameter. In our example, we passed in an ValueParameter instance, which is an implementation of IParameter that stores the value in the class and returns it when needed. Once the value for the property is obtained, the InjectProperties method checks to make sure that the value of the property can be assigned to the property and then it calls PropertyInfo.SetValue.

 

That's the details on DI via properties. With this much detail, it is easy to lose focus on the big picture, so lets understand the design of this aspect of the OB. The figure below dipicts the design of DI via properties in OB.

 

As shown, there are really three core interfaces involved in the design, IPropertySetterPolicy, IPropertySetterStrategy, and IPropertySetterInfo. The strategy class looks for a registered IPropertySetterPolicy at build-up for a given type. If the policy is registered for the type, the strategy iterates the properties that require injection. Every injection property is represented by an IPropertySetterInfo. IPropertySetterInfo encapsulates two things about the property: what the property is and how to get a value for the property. OB provides an implementation of this interface, PropertySetterInfo, that operates on an IParameter abstraction (see PropertySetterInfo class below).

 

public class PropertySetterInfo : IPropertySetterInfo
{
 string name = null;
 PropertyInfo prop = null;
 IParameter value = null;

 

 public PropertySetterInfo(string name, IParameter value)
 {
  this.name = name;
  this.value = value;
 }

 

 public PropertySetterInfo(PropertyInfo propInfo, IParameter value)
 {
  this.prop = propInfo;
  this.value = value;
 }

 

 public PropertyInfo SelectProperty(IBuilderContext context, Type type, string id)
 {
  if (prop != null)
   return prop;

 

  return type.GetProperty(name);
 }

 

 public object GetValue(IBuilderContext context, Type type, string id, PropertyInfo propInfo)
 {
  return value.GetValue(context);
 }
}

 

As shown, at a minimum, you need to know two things to get a value injected into a property: 1) the name of the property and 2) how to get the value for the property. IParameter provides an abstraction to obtaining, and customizing, how the PropertySetterStrategy obtains the value to assign to the property. In our example earlier, for example, we used a ValueParameter extension that stored the value of the ConnectionString in the class and returned it when the strategy asked for it. The design of DI via properties also shows that OB provides a number of other IParameter extensions. For example, the LookupParameter implement