Monday 28 May 2012

Invoking Soa Suite 11g Service from java

Invoking Soa Suite 11g Service from java

In Soa Suite 11g we can not call the composite service directly from java. We need to copy the service in the composite, change its binding to adf and wire this service to the component. All the credits goes to Jay's Blog and Clemens, Great work.

The first step is to open the composite xml and find your service.
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Generated by Oracle SOA Modeler version 1.0 at [8/25/09 3:01 PM]. -->
<composite name="Helloworld"
           revision="1.0"
           label="2009-08-25_15-01-51_078"
           mode="active"
           state="on"
           xmlns="http://xmlns.oracle.com/sca/1.0"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"
           xmlns:orawsp="http://schemas.oracle.com/ws/2006/01/policy"
           xmlns:ui="http://xmlns.oracle.com/soa/designer/">
  <import namespace="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1"
          location="BPELProcess1.wsdl" importType="wsdl"/>
   <service name="bpelprocess1_client_ep" ui:wsdlLocation="BPELProcess1.wsdl">
     <interface.wsdl interface="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.interface(BPELProcess1)"/>
      <binding.ws port="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.endpoint(bpelprocess1_client_ep/BPELProcess1_pt)">
      <wsp:PolicyReference URI="oracle/log_policy" orawsp:category="management"
                           orawsp:status="enabled"/>
    </binding.ws>
   </service>

Copy this service and give it a unique name and now we need to add the binding.adf binding to this service instead of the binding.ws
<service name="bpelprocess1_client_ep" ui:wsdlLocation="BPELProcess1.wsdl">
     <interface.wsdl interface="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.interface(BPELProcess1)"/>
      <binding.ws port="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.endpoint(bpelprocess1_client_ep/BPELProcess1_pt)">
      <wsp:PolicyReference URI="oracle/log_policy" orawsp:category="management"
                           orawsp:status="enabled"/>
    </binding.ws>
</service>
<service name="ADFBindingService" ui:wsdlLocation="BPELProcess1.wsdl">
     <interface.wsdl interface="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.interface(BPELProcess1)"/>
    <binding.adf serviceName="{http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1}ADFBindingService"
                 registryName=""/>
</service>

Go back to the design mode and open the new adf binding service and select the same wsdl as your other service ( this will correct the serviceName ) and at last we need to wire the new service to the component

Now we only need to call this service from java
package nl.whitehorses.soa.client;
import java.util.Hashtable;
import java.util.UUID;
import java.util.List;
import javax.naming.Context;
import oracle.soa.management.facade.Locator;
import oracle.soa.management.facade.LocatorFactory;
import oracle.soa.management.facade.Composite;
import oracle.soa.management.facade.Service;
import oracle.soa.management.facade.CompositeInstance;
import oracle.soa.management.facade.ComponentInstance;
import oracle.fabric.common.NormalizedMessage;
import oracle.fabric.common.NormalizedMessageImpl;
import oracle.soa.management.util.CompositeInstanceFilter;
import oracle.soa.management.util.ComponentInstanceFilter;
import java.util.Map;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
import org.w3c.dom.Element;
import java.io.*;
public class StartProcess {
    public StartProcess() {
        super();
        Hashtable jndiProps = new Hashtable();
        jndiProps.put(Context.PROVIDER_URL, "t3://soaps3.alfa.local:8001/soa-infra");
        jndiProps.put(Context.INITIAL_CONTEXT_FACTORY,"weblogic.jndi.WLInitialContextFactory");
        jndiProps.put(Context.SECURITY_PRINCIPAL, "weblogic");
        jndiProps.put(Context.SECURITY_CREDENTIALS, "weblogic1");
        jndiProps.put("dedicated.connection", "true");
        String inputPayload =
        "<process xmlns=\"http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1\">" +
        " <input>hello</input>" +
        "</process>" ;
        Locator locator = null;
        try {
            // connect to the soa server
            locator = LocatorFactory.createLocator(jndiProps);
            String compositeDN = "default/Helloworld!1.0";
            // find composite
            Composite composite = locator.lookupComposite("default/Helloworld!1.0");
            
            System.out.println("Got Composite : "+ composite.toString());
            // find exposed service of the composite
            Service service = composite.getService("ADFBindingService");
            System.out.println("Got serviceName : "+ service.toString());
            // make the input request and add this to a operation of the service
            NormalizedMessage input = new NormalizedMessageImpl();
            String uuid = "uuid:" + UUID.randomUUID();
            input.addProperty(NormalizedMessage.PROPERTY_CONVERSATION_ID,uuid);
            // payload is the partname of the process operation
            input.getPayload().put("payload",inputPayload);
            // process is the operation of the employee service
            NormalizedMessage res = null;
            try {
               res = service.request("process", input);
            } catch(Exception e) {
               e.printStackTrace();
            }
            Map payload = res.getPayload();
            Element element = (Element)payload.get("payload");
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer();
            transformer.setOutputProperty("indent", "yes");
            StringWriter sw = new StringWriter();
            StreamResult result = new StreamResult(sw);
            DOMSource source = new DOMSource(element);
            transformer.transform(source, result);
            System.out.println("Result\n"+sw.toString());
            System.out.println("instances");
            
            
            CompositeInstanceFilter filter = new CompositeInstanceFilter();
            filter.setMinCreationDate(new java.util.Date((System.currentTimeMillis() - 2000000)));
            // get composite instances by filter ..
            List<CompositeInstance> obInstances = composite.getInstances(filter);
            // for each of the returned composite instances..
            for (CompositeInstance instance : obInstances) {
                System.out.println(" DN: " + instance.getCompositeDN() +
                                   " Instance: " + instance.getId() +
                                   " creation-date: " + instance.getCreationDate() +
                                   " state (" + instance.getState() + "): " + getStateAsString(instance.getState())
                                   );
                                  
                // setup a component filter
                ComponentInstanceFilter cInstanceFilter = new ComponentInstanceFilter();
                // get child component instances ..
                List<ComponentInstance> childComponentInstances = instance.getChildComponentInstances(cInstanceFilter);
                // for each child component instance (e.g. a bpel process)
                for (ComponentInstance cInstance : childComponentInstances) {
                    System.out.println(" -> componentinstance: " + cInstance.getComponentName() +
                                       " type: " + cInstance.getServiceEngine().getEngineType() +
                                       " state: " +getStateAsString(cInstance.getState())
                                       );
                    System.out.println("State: "+cInstance.getNormalizedStateAsString() );
                }
            }
  
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private String getStateAsString(int state)
    {
        // note that this is dependent on wheter the composite state is captured or not
        if (state == CompositeInstance.STATE_COMPLETED_SUCCESSFULLY)
            return ("success");
        else if (state == CompositeInstance.STATE_FAULTED)
            return ("faulted");
        else if (state == CompositeInstance.STATE_RECOVERY_REQUIRED)
            return ("recovery required");
        else if (state == CompositeInstance.STATE_RUNNING)
            return ("running");
        else if (state == CompositeInstance.STATE_STALE)
            return ("stale");
        else
            return ("unknown");
    }
    public static void main(String[] args) {
        StartProcess startUnitProcess = new StartProcess();
    }
} /////////////////////////////////////  jars to be downloaded and imported MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.mgmt_11.1.1\soa-infra-mgmt.jar MiddlewareJdev11gR1PS1\oracle_common\modules\oracle.fabriccommon_11.1.1\fabric-common.jar MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.fabric_11.1.1\fabric-client.jar MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.fabric_11.1.1\fabric-ext.jar MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.fabric_11.1.1\fabric-runtime.jar MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.fabric_11.1.1\oracle-soa-client-api.jar MiddlewareJdev11gR1PS1\jdeveloper\soa\modules\oracle.soa.fabric_11.1.1\oracle.soa.fabric.jar ////////////////////////////////////

Invoking Soa Suite 11g Service from java

Calling a Soa Suite Direct Binding Service from Java & OSB

I was trying to connect Oracle Soa Suite 11G R1 PS1 with the OSB when I saw this new Direct Binding Service in the Soa Suite 11G. This direct binding make it possible to start this RMI service from OSB or Java. In a previous blog I already called a Soa Service from Java using the ADF binding but this direct binding makes it also possible to call this also from OSB using the SB transport . In this Blog I will call this RMI synchronous service from Java, I can not use this binding in OSB 10.3.1, probably in the next version of the OSB I can.

First we add the Direct Binding Service to exposed Services side of the composite and use the WSDL of one of the other exposed services and add a Wire to the Component.
In the source view of the composite xml you can see that this service uses the direct binding.
  1. <service name="RMIService" ui:wsdlLocation="BPELProcess1.wsdl">  
  2. <interface.wsdl interface="http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1#wsdl.interface(BPELProcess1)"/>  
  3. <binding.direct/>  
  4. </service>  

To see the WSDL of this service go to http://localhost:8001/soa-infra/ and select your RMI service.

package nl.whitehorses.soa.client;
import java.io.StringWriter;
import java.io.StringReader;
import java.util.Hashtable;
import java.util.Map;
import java.util.HashMap;
import javax.naming.Context;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import oracle.soa.api.PayloadFactory;
import oracle.soa.api.XMLMessageFactory;
import oracle.soa.api.invocation.DirectConnection;
import oracle.soa.api.message.Message;
import oracle.soa.api.message.Payload;
import oracle.soa.management.CompositeDN;
import oracle.soa.management.facade.Locator;
import oracle.soa.management.facade.LocatorFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.Document;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.xml.sax.InputSource;
public class StartRMIProcess {
    public StartRMIProcess() {
        super();
        Hashtable jndiProps = new Hashtable();
        jndiProps.put(Context.PROVIDER_URL, "t3://soaps3.alfa.local:8001/soa-infra");
        jndiProps.put(Context.INITIAL_CONTEXT_FACTORY,"weblogic.jndi.WLInitialContextFactory");
        jndiProps.put(Context.SECURITY_PRINCIPAL, "weblogic");
        jndiProps.put(Context.SECURITY_CREDENTIALS, "weblogic1");
        jndiProps.put("dedicated.connection", "true");
        Locator locator = null;
        try {
            // connect to the soa server
            locator = LocatorFactory.createLocator(jndiProps);
            // find composite default domain, Helloworld Composite, version 1.0
            CompositeDN compositedn = new CompositeDN("default", "Helloworld", "1.0");
            // call the direct binding of the Helloworld composite
            DirectConnection conn = locator.createDirectConnection(compositedn,"RMIService");
            
            String inputPayload =
            "<client:process xmlns:client=\"http://xmlns.oracle.com/HelloWorld/Helloworld/BPELProcess1\">\n" +
            " <client:input>hello</client:input>\n" +
            "</client:process>\n" ;
            
            DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = docBuilderFactory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(inputPayload)));
            Element root = doc.getDocumentElement();
            //<wsdl:message name="BPELProcess1RequestMessage">
            // <wsdl:part name="payload" element="client:process"/>
            //</wsdl:message>
            Map<String, Element> partData = new HashMap<String,Element>();
            // have to use payload see BPELProcess1RequestMessage
            partData.put("payload", root);
            Payload<Element> payload = PayloadFactory.createXMLPayload(partData);
            //Messages are created using the MessageFactory
            Message<Element> request = XMLMessageFactory.getInstance().createMessage();
            request.setPayload(payload);
            //<wsdl:portType name="BPELProcess1">
            // <wsdl:operation name="process">
            // <wsdl:input message="client:BPELProcess1RequestMessage" />
            // <wsdl:output message="client:BPELProcess1ResponseMessage"/>
            // </wsdl:operation>
            //</wsdl:portType>
            // this is a request-reply service so we need to use conn.request else use conn.post
            // need to provide operation name so we need to use process
            Message<Element> response = conn.request("process", request);
            TransformerFactory tFactory = TransformerFactory.newInstance();
            Transformer transformer = tFactory.newTransformer();
            transformer.setOutputProperty("indent", "yes");
            StringWriter sw = new StringWriter();
            StreamResult result = new StreamResult(sw);
            //<wsdl:message name="BPELProcess1ResponseMessage">
            // <wsdl:part name="payload" element="client:processResponse"/>
            //</wsdl:message>
            // need to use payload again
            DOMSource source = new DOMSource((Node)response.getPayload().getData().get("payload"));
            transformer.transform(source, result);
            System.out.println("Result\n"+sw.toString());
        
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        StartRMIProcess startRMIProcess = new StartRMIProcess();
    }
}

Thursday 17 May 2012

Pass Variables between Java & BPEL Process


When using Java embedding activity in a BPEL process for processing BPEL variables , it will be required to pass the variables between Java and the BPEL process. In such cases, you can use
the following methods to get/set the variables;

getVariableData(bpelVariable); method will fetch that variable value to be used inside the Java embedding activity

setVariableData(bpelVariable, Value); method sets the Value (processed in the Java embedding activity) to the bpelVariable, which can be used in the BPEL process for further processing.

However, the variable datatype conversions should be properly handled in BPEL process and Java embedding activity.

Lost BPEL Instances? - Use Force Dehydration


There will be many instances when it will be really puzzling to find out whether the BPEL instance was triggered or not, where we often say that the "BPEL instance is lost". Actually, in reality, the BPEL process could have resulted in some kind of exception during the process which gets suppressed completely by the BPEL PM and the instance neither gets persisted or dehydrated in the orabpel schema nor shown up in BPELConsole.

This will be really troublesome, as we will not be able to identify whether the problem lies in the external system driving the BPEL instance or within the BPEL process itself after consumption of data. In such cases, you can use the checkpoint(); Java method inside a Java embedding activity to force the instance to be dehydrated as soon as the instance gets triggered in BPEL. This will ensure that the BPEL instance is shown up in the BPEL console if it is instantiated. You can use the following piece of code in your BPEL process to force dehydration.

<bpelx:exec name="CheckPointOne" language="java" version="1.5">
<![CDATA[checkpoint();]]>
</bpelx:exec>

Now, all your BPEL instances will be available in the BPELConsole for debugging issues that are not actually propagated/thrown to BPELConsole by the OPMN.

However, there are certain drawbacks while using the force dehydration mechanism, as this will cause drop in performance of the BPEL process during run-time due to the additional overhead of persisting the instance whenever this method is called. So please make justified use of this method before using this.

Friday 4 May 2012

DB ADAPTER


Extended Use Case

In this article I will extend the use case from Part 1 by an Inbound Database Adapter, which should poll the database for changes.  The extended scenario is shown in the image below using the notation from the Integration Blueprint book. The elements shown in blue are the new ones added to the use case from part 1.
 000_use-case
The Database Adapter will be configured to listen on the PERSON_CHG_T helper table for new records. This table is filled by a trigger on the PERSON_T table and will hold one row for every change to the PERSON_T table.
For each new row in PERSON_CHG_T I want an new OSB service to be called. This new service will use the data from the inbound request and enrich it by re-using the PersonService proxy service we have built in Part 1.

Prerequisites

The prerequisites for the 2nd part are obviously the same as in Part 1. The following software needs to be installed and available:
  • JDeveloper 11g with SOA extension
  • Eclipse 3.5.2 with Oracle Enterprise Pack for Eclipse (OEPE) 11.1.1.5.0
  • Oracle Service Bus 11.1.1.3
  • Oracle Database (XE is good enough)
Additionally you need the completed OSB project from Part 1. The solution can be downloaded from here.

Project Setup

The project setup has been done in Part 1. We will reuse the same Eclipse OSB project with the nested JDeveloper SOA Project and just continue where we have left in Part 1.

Create the Inbound Database Adapter

First let’s create a new Database Adapter.
For that we don’t need a new JDeveloper project, we can reuse the same project we created in Part 1, wrapped inside the adapter folder. I think it’s a good practice to keep all the adapters necessary for one OSB project in only one JDeveloper project.
Let’s go to JDeveloper an open the composite.xml to show the SCA composite view.
  1. Drag a new Database Adapter into the SCA composite. Because it’s an Inbound Adapter, we will use the left hand swimmlane named “Exposed Services” for that. This is not strictly necessary when using the OSB but I think it’s a good mnemonic trick to do so (organizing inbound adapters on the left and outbound adapters on the right, as discussed in part 1).
    010_drag-db-adapter
  2. Give the adapter service a good and meaningful name:
    015_adapter-wizard-2of4
  3. For the connection we reuse the settings already their from part 1, so we can move forward to the the Operation Type selection. This time we want to use the Database Adapter to “Poll for New or Changed Records in a Table”.
    020_adapter-wizard-4of5
  4. We want to poll the PERSON_CHG_T table, so let’s select it. 025_adapter-wizard-5of12
  5. We can see that the table only holds an ID and a timestamp. So that’s all we get in the inbound message, whenever a row is inserted into PERSON_CHG_T. This is the reason why we later want to enrich the message with more information in a second step.
     030_adapter-wizard-7of12
  6. Next we need to define the strategy to use for signaling that a row has been read and successfully processed by the adapter. Because PERSON_CHG_T is a helper table no one else is using, it’s fine to just delete the row.
     035_adapter-wizard-8of12
  7. Next the Polling Options can be specified. Among others you can specify the polling frequency, which is set to 5 seconds by default, meaning that the Adapter will do the SQL operation shown on the right every 5 seconds. For our sample that’s fine, but in real world you should of course set it to a value matching your requirements.
    045_adapter-wizard-9of12
  8. Last but not least the Database Adapter allows for setting a selection criteria. We don’t use it this time, as we want to read all the rows which are added to the PERSON_CHG_T. 050_adapter-wizard-10of12
This finishes the creation of the Inbound Database Adapter and our work in JDeveloper. We can see the Adapter on the right hand swimmlane.
 055_composite-with-new-adapter
The adapter is now prepared to poll the PERSON_CHG_T table for new records every 5 seconds. Each row being read will be send to the service linked to the adapter. So let’s switch to the OSB project in Eclipse and create a new service to handle these messages.

Creating the OSB Service and linking it to the Inbound Database Adapter

When working with Inbound Adapters, an OSB proxy service needs to be used. The adapter will invoke the proxy service whenever a new message “is created” by the adapter.
  1. In order to be able to create/generate the proxy service, we need the new adapter artifacts in Eclipse. Just do a refresh on the adapter folder and they will show up. 060_osb-project-refresh
  2. No we can choose Generate Service on the JCA configuration file (PollingPersonService_db.jca) to create the necessary OSB service.
    065_generate-proxy-service-for-jca
  3. Based on the JCA settings, OSB knows that it is an Inbound Adapter and will generate a JCA Proxy Service automatically. All we need to specify is the right folder: 070_name-proxy-service
  4. The proxy settings, created for you, show that a WSDL is used which has been generated as well:
    075_proxy-general-tab
  5. The transport setting show the usage of the JCA Transport:
    080_proxy-transport-tab
  6. All we need to do is specify what should happen with the message, by defining a meaningful Message Flow. For a start add a Pipeline Pair Node with a nested Stage Node and a Log action to show the message in the OSB log on the console. Make sure to specify a Severity level in the Log action which is shown in the log. If you are unsure what to choose, then “Error” will be fine for that sample and shown by default.
    085_adding-log-to-proxy
  7. Now let’s deploy the OSB project and test if the Inbound Adapter works. For that let’s open SQL PLus, connect to SOA_SAMPLE and do an UPDATE on the PERSON_T table. By that the trigger on that table will fire and signal the change by adding a row to the PERSON_CHG_T table. Make sure to commit the change! 090_testing-with-sql-plus-chg
  8. After a maximum of 5 seconds (remember the polling frequency specified in the Database Adapter wizard) the log should show up on the OSB console window. 095_testing-with-sql-plus-chg-2

We can see that the polling Database Adapter worked. A message has been sent to the OSB proxy service holding the ID of the changed PERSON_T row and a timestamp!
In a real world scenario you would now want to do something more meaningful with this information than just logging it to the console, i.e. you want to inform another system about the change. In order to do that, you might need to send more information than just the ID of the person. The system to inform maybe require the person information, similar to the information returned by the PersonService we developed in Part 1. So let’s reuse that proxy service to enrich our message, implementing the Content Enricher design pattern.

Adding the Content Enricher

To enrich our message, we want to call the PersonService proxy service from the Message Flow of the PollingPersonServiceDB proxy service.
  1. Let’s add a Service Callout action and rename the stage to EnrichmentStage. It’s always a good idea to meaningfully name the different nodes used to structure the message flow. This helps you to better understand and document your message flow at development time but also helps in case of errors at runtime, to easier identify the place where the error occurred.
     100_adding-service-callout
  2. Configure the Service Callout action to call the PersonService proxy and to invoke the findPerson operation. For the request and response message we define two variables and specify to use a Soap Body. The Service Callout action allows to use separate variables for the request and response message. By that the content of the $body variable from the request to the proxy service stays untouched during the service callout. This is important if you want to merge the response from the service callout with the original request. This is not necessary in our simple example, all we will use is the response directly from the service callout. But usually you will need to merge the two when implementing the Content Enricher pattern in OSB.
     105_configure-service-callout
  3. Next we implement the Assign action to set the requestBody variable. 
     112_add-assign-for-request-2
  4. Because we specified “Configure Soap Body” in the Service Callout properties, we need to setup the <findPersonRequest> message wrapped in a <soap-env:Body> element. The value of the <personId> element can be retrieved from the $body variable by dragging it into the Expression view and defining the XPath expression shown in the image below
    115_configure-assign-for-request  
    117_configure-assign-for-request-2
  5. Last but not least you need to add v1 as a custom namespace:
    120_add-namespace
  6. In the Response Action of the Service Callout we will also use an Assign action, this time to copy the value of the $responseBody variable to the $body variable.
     130_configure-assign-for-response
  7. Let’s change the Annotation of the Log action from before to state the fact that we now log the content of the $body variable after the service callout has been made.
    135_change-log-action
  8. Let’s test it in the same way as before. Just re-execute the UPDATE on PERSON_T and this time a longer log message with a complete Person instance should be shown.
    140_testing-with-sql-plus-2

The Content Enrichment worked an the complete and up-to-date person information could now be sent to any system interested.

Conclusion

This finishes the 2nd part of this blog article series.
We have added an Inbound Adapter to the use case to get informed whenever the information changes in the PERSON_T table. By re-using the PersonService from a Service Callout in the Message Flow we were able to enrich the incoming message to a more meaningful “change message”, which could now be used to inform potential external systems of changes happening on the PERSON_T table.
We have used the OSB to implement parts of a typical integration scenario. Similar to one of the scenarios documented in our Integration Blueprint book!
The implementation of a dynamic publish-subscribe mechanism on the OSB, in order to inform the systems interested could be a topic of a next blog article.
The source code for the solution can be downloaded from here. I will again provide a video showing how this extension of the use case has been developed.

xslt padding with characters call template for left pad and right pad

  Could a call-template be written that took two parameters ?   a string, and a   number) return the string with empty spaces appended t...