2007/12/17
17 Dec, 2007

[Article] Writing a Mediator in WSO2 ESB - Part 2

  • Upul G
  • - WSO2

This tutorial shows you how to take a custom mediator you have written and add a Domain Specific XML configuration snippet for it. This approach is called Domain Specific Modelling (DSM) and helps make your Apache Synapse and WSO2 ESB mediators simpler and easier to re-use. You can read the first part here.

Introduction

Writing a Mediator in ESBIn part 1 of this series, we showed you how to write a custom mediator using the class Extension Mediator. We used the <class /> configuration element and gave the custom mediator implementation class name and the values for properties. Mediator definition in the configuration was as follows:

 <class name="org.wso2.esb.tutorial.mediators.SurchargeStockQuoteMediator">
<property name="defaultPercentage" value="10"/>
</class>

 

Using this method, WSO2 ESB instantiates an object of org.wso2.esb.tutorial.mediators.SurchargeStockQuoteMediator and sets Java bean properties as given by <property /> elements. In this particular case WSO2 ESB created a SurchargeStockQuoteMediator instance and called setDefaultPercentage(String) method with an argument “10” of String type. Then mediate() method of the SurchargeStockQuoteMediator was called when the message traveled in the mediation path.

 

Modified Surcharge Stock Quote Mediator

In this article we will show you how to use a complex XML configuration model to initialize the mediator instance. In addition to the default surcharge percentage, we will also initialize the surcharges map using configuration values. The XML configuration will be as the following:

<surchargeStockQuote>
<defaultPercentage>10</defaultPercentage>
<surcharges>
<surcharge symbol="IBM">15</surcharge>
<surcharge symbol="MSFT">20</surcharge>
<surcharge symbol="SUN">25</surcharge>
</surcharges>
</surchargeStockQuote>

 

This way, we can use a rich XML model with elements and attributes to initialize the mediator without being limited to Java bean style String type and OMElement type properties. In addition we can make the configuration model more "human-readable", improving the ability for this mediator to be re-used.

Firstly, we will change the surcharge stock quote mediator in part 1, slightly, by adding addSurcharge(), getSurcharges() methods. However, the more significant addition will be the inclusion of mediator factory and the mediator serializer. The Mediator Factory class reads the mediator configuration from the XML and initializes the mediator object. The mediator serializer can write out the configuration of a mediator object to the XML format - which is used when the UI needs to save the configuration.

We can achieve the same functionality using the class mediator and using OMElement properties which accept XML elements. But using this factory method we can clearly separate the mediator initialization code to the factory class and the functionality to the mediator class.

Our mediator class is given below. Surcharges map is populated by the Mediator Factory when a mediator object is created. So we have added a method to the mediator to add surcharge values to the surcharges map.

 

public class SurchargeStockQuoteMediator extends AbstractMediator {

  // symbol -> surcharge percentage
  private Map<String, Double> surcharges = new HashMap<String, Double>();
  // default surcharge percentage to use if symbol not found in surcharges map
  private Double defaultPercentage = 0d;

  public boolean mediate(MessageContext synCtx) {

    // establish log levels
    boolean traceOn = logger.isTraceEnabled();
    boolean traceOrDebugOn = logger.isTraceOrDebugEnabled();

    // write log messages
    if (traceOrDebugOn) {
      logger.traceOrDebug("Start : SurchargeStockQuote mediator");

      if (traceOn && trace.isTraceEnabled()) {
        trace.trace("Message : " + synCtx.getEnvelope());
      }
    }

    // get symbol, last elements of SOAP envelope
    SOAPBody body = synCtx.getEnvelope().getBody();
    OMElement firstElement = body.getFirstElement();

    OMElement symbolElement = null;
    try {
      AXIOMXPath xPathSymbol = new AXIOMXPath("//ns:symbol");
      xPathSymbol.addNamespace("ns", "https://services.samples/xsd");
      symbolElement = (OMElement) xPathSymbol.selectSingleNode(firstElement);

    } catch (JaxenException e) {
      handleException("element symbol error", e, synCtx);
    }

    if (symbolElement == null) {
      handleException("element symbol not found", synCtx);
    }

    OMElement lastElement = null;
    try {
      AXIOMXPath xPathLast = new AXIOMXPath("//ns:last");
      xPathLast.addNamespace("ns", "https://services.samples/xsd");
      lastElement = (OMElement) xPathLast.selectSingleNode(firstElement);

    } catch (JaxenException e) {
      handleException("element last error", e, synCtx);
    }

    if (lastElement == null) {
      handleException("element last not found", synCtx);
    }

    // lookup surcharge percentage from surcharges map
    // if not found apply default surcharge percentage
    Double surchargePercentage = getDefaultPercentage();
    String symbol = symbolElement.getText();
    Double givenSurchargePercentage = surcharges.get(symbol);
    if (givenSurchargePercentage != null) {
      surchargePercentage = givenSurchargePercentage;
    }

    String text = lastElement.getText();
    Double price = Double.valueOf(text);

    Double newPrice = price.doubleValue()
        + (price.doubleValue() * surchargePercentage.doubleValue() / 100);
    
    // write back new stock price
    lastElement.setText(String.valueOf(newPrice));

    // print log message
    log.info("symbol:" + symbol + " original price:" + price + " surcharge:"
        + surchargePercentage + "% new price:" + newPrice);
    
    // write log messages
    if (traceOrDebugOn) {
      logger.traceOrDebug("End : SurchargeStockQuote mediator");
    }

    // proceed with next mediator
    return true;
  }

  public Double getDefaultPercentage() {
    return defaultPercentage;
  }

  public void setDefaultPercentage(double defaultPercentage) {
    this.defaultPercentage = defaultPercentage;
  }
  
  public Map<String, Double> getSurcharges() {
    return surcharges;
  }

  public void setSurcharges(Map<String, Double> surcharges) {
    this.surcharges = surcharges;
  }

  public void addSurcharge(String symbol, Double percentage) {
    this.surcharges.put(symbol, percentage);
  }
}


Writing the Mediator Factory

Our custom Mediator Factory class is given below. The main method that needs to be implemented is the createSpecificMediator(OMElement el) method. Simply, the ESB configuration system will take the XML fragment and pass it to the MediatorFactory, which will return a fully configured mediator instance. You might ask - "how does the Synapse configuration model know, which XML fragments to be passed over to this particular factory?" - we will look at the answer to that in a minute.

 

public class SurchargeStockQuoteMediatorFactory extends AbstractMediatorFactory {

static final QName SURCHARGE_STOCK_QUOTE_Q = new QName(
XMLConfigConstants.SYNAPSE_NAMESPACE, "surchargeStockQuote");

static final QName DEFAULT_PERCENTAGE_Q = new QName(
XMLConfigConstants.SYNAPSE_NAMESPACE, "defaultPercentage");

static final QName SURCHARGES_Q = new QName(
XMLConfigConstants.SYNAPSE_NAMESPACE, "surcharges");

static final QName SURCHARGE_Q = new QName(
XMLConfigConstants.SYNAPSE_NAMESPACE, "surcharge");

static final QName SYMBOL_Q = new QName("symbol");

public Mediator createSpecificMediator(OMElement elem) {

// create new mediator
SurchargeStockQuoteMediator newMediator = new SurchargeStockQuoteMediator();

// setup initial settings
processTraceState(newMediator, elem);

// read default surcharge percentage
OMElement defaultPercentageElement = elem.getFirstChildWithName(DEFAULT_PERCENTAGE_Q);
if (defaultPercentageElement != null) {
Double defaultPercentage = Double.valueOf(defaultPercentageElement.getText());
newMediator.setDefaultPercentage(defaultPercentage);

} else {
throw new SynapseException("default percentage element missing");
}

// read given surcharges and add them to the map of the mediator
// symbol -> percentage
OMElement surchargesElement = elem.getFirstChildWithName(SURCHARGES_Q);
if(surchargesElement != null) {
Iterator surchargesIter = surchargesElement.getChildrenWithName(SURCHARGE_Q);
while (surchargesIter.hasNext()) {
OMElement surchargeElem = (OMElement) surchargesIter.next();
OMAttribute symbolAtr = surchargeElem.getAttribute(SYMBOL_Q);
if(symbolAtr != null) {
String symbol = symbolAtr.getAttributeValue();

String percentageValue = surchargeElem.getText();
Double percentage = Double.valueOf(percentageValue);

if(symbol != null && percentage != null) {
newMediator.addSurcharge(symbol, percentage);
}
} else {
throw new SynapseException("symbol attribute missing");
}
}
}

return newMediator;
}

public QName getTagQName() {
return SURCHARGE_STOCK_QUOTE_Q;
}
}

 

When the WSO2 ESB starts, it reads the configuration XML file and builds an object model of given mediators. For example, when ESB sees a <syn:surchargeStockQuote/> element when processing the configuration XML, it will look up its list of mediator factory objects for a mediator factory which can handle <syn:surchargeStockQuote/> element. We have to write a class which implements MediatorFactory interface for this purpose. To make it simpler, there is a convenience class, AbstractMediatorFactory, which you can use to extend SurchargeStockQuoteMediatorFactory. We will explain how the ESB finds all Mediator Factories, later on in this article.

The other important method that must be implemented is getTagQName(). This method returns tagname of the elements which this mediator factory can deal with. When the ESB  loads, it creates a lookup table of element names to factories. If it sees the <syn:surchargeStockQuote/> element, it will know that the right factory to call is SurchargeStockQuoteMediatorFactory. It will then call the mediator factory method, Mediator createSpecificMediator(OMElement element) for each <syn:surchargeStockQuote/> found in the configuration. createSpecificMediator() should read <syn:surchargeStockQuote/> and its child elements and configure a mediator object and return. This way, the created element can be customized by XML elements and attributes given in the configuration file. For example, here we are reading a child element called <syn:defaultPercentage/> and initializing the defaultPercentage member instance of the created mediator object. We are also using the <syn:surcharges /> element to initialize surcharges map object. As you could see, this gives a lot of flexibility by allowing the mediator factory to define any XML fragment it needs to support any kind of configuration.

 

Writing the Mediator Serializer

The serializer does the opposite to the factory. It supports writing out a mediator's configuration to XML. Our custom mediator serializer class is given below:

 

public class SurchargeStockQuoteMediatorSerializer extends
AbstractMediatorSerializer {

public OMElement serializeSpecificMediator(OMElement parent, Mediator m) {

if (!(m instanceof SurchargeStockQuoteMediator)) {
handleException("Unsupported mediator passed in for serialization : "
+ m.getType());
}

SurchargeStockQuoteMediator mediator = (SurchargeStockQuoteMediator) m;
OMElement surchargeStockQuoteElement = fac
.createOMElement(SurchargeStockQuoteMediatorFactory.SURCHARGE_STOCK_QUOTE_Q);
parent.addChild(surchargeStockQuoteElement);

saveTracingState(surchargeStockQuoteElement, mediator);

// add default percentage element
OMElement defaultElement = fac.createOMElement(
SurchargeStockQuoteMediatorFactory.DEFAULT_PERCENTAGE_Q, surchargeStockQuoteElement);
defaultElement.setText(String.valueOf(mediator.getDefaultPercentage()));

OMElement surchargesElement = fac
.createOMElement(SurchargeStockQuoteMediatorFactory.SURCHARGES_Q, surchargeStockQuoteElement);

// add symbol -> percentage list
Map surcharges = mediator.getSurcharges();
for(String symbol : mediator.getSurcharges().keySet()) {
Double percentage = (Double) surcharges.get(symbol);

OMElement surchargeElement = fac
.createOMElement(SurchargeStockQuoteMediatorFactory.SURCHARGE_Q, surchargesElement);

surchargeElement.addAttribute(fac.createOMAttribute("symbol", nullNS, symbol));

surchargeElement.setText(String.valueOf(percentage));
}

return surchargeStockQuoteElement;
}

public String getMediatorClassName() {
return SurchargeStockQuoteMediator.class.getName();
}
}

 

Mediator serializer class converts a mediator object to XML format. This is used when saving the current mediator object model to XML format when saving the configuration to disk. This is the reverse action of what Mediator Factory does. Mediator serializer class should implement org.apache.synapse.config.xml.MediatorSerializer. Just as we did before, there is a convenience class, AbstractMediatorSerializer which you can use instead of MediatorSerializer interface.

You have to implement OMElement serializeSpecificMediator(OMElement parent, Mediator m) where the mediator and the parent element of the XML tree is passed to the mediator. In serializeSpecificMediator() method, your task is to build the XML model that represent the current settings of the mediator object. For example, ihere we are serializing defaultPercentage member as a child element.

The XML element generated by the mediator serializer should be readable by the mediator factory to create a mediator object again.

Additionally, you should implement String getMediatorClassName() to return the full class name of the mediator. This is how the ESB will know which mediator serializer class to be used for each mediator object it finds when serializing a mediator object model.

 

Packaging the Custom Mediator

WSO2 ESB uses Java Service Provider mechanism to discover available mediator factories and mediator serializers. This is a standard approach from Java that is used by other APIs just as JAXP. It allows us to package our mediator, factory and serializer as a simple JAR file, and all that is needed afterwards is to "register" our new addition iin the classpath. The WSO2 ESB  goes through META-INF/services directories of all the JAR files in class path and will search for the following files: Files named org.apache.synapse.config.xml.MediatorFactory will list the MediatorFactory derived class names available. The files named org.apache.synapse.config.xml.MediatorSerializer will list the MediatorSerializer derived class names available. WSO2 ESB will load listed mediator factory classes when starting up. Each mediator factory class advertises which type of element it can handle by the QName getTagQName() method. When ESB sees <syn:surchargeStockQuote/> element in the configuration file, this is how the createSpecificMediator() method of the SurchargeStockQuoteMediatorFactory is called.

Here is the final packaging structure of the custom mediator:

+- org
+- wso2
+- esb
+ tutorial
+- mediators
+- SurchargeStockQuoteMediator.class
+- SurchargeStockQuoteMediatorFactory.class
+- SurchargeStockQuoteMediatorSerializer.class
+- META-INF
+- services
+- org.apache.synapse.config.xml.MediatorSerializer
+- org.apache.synapse.config.xml.MediatorFactory

 

The Surcharge stock quote mediator is packaged as above in a JAR file. The two resource files should go to META-INF/services directory. The completed JAR file now just needs to be added to the ESB class path. You can put that in the lib directory of ESB, so that the next time ESB starts, the new mediator will be ready for use.

 

Testing the Mediator

We are going to use the following ESB configuration .

 

<definitions xmlns="http://ws.apache.org/ns/synapse">
<proxy name="StockQuoteProxy">
<target>
<endpoint>
<address uri="https://localhost:9000/soap/SimpleStockQuoteService"/>
</endpoint>
<outSequence>
<surchargeStockQuote>
<defaultPercentage>10</defaultPercentage>
<surcharges>
<surcharge symbol="IBM">15</surcharge>
<surcharge symbol="MSFT">20</surcharge>
<surcharge symbol="SUN">25</surcharge>
</surcharges>
</surchargeStockQuote>
<send/>
</outSequence>
</target>
<publishWSDL uri="file:repository/conf/sample/resources/proxy/sample_proxy_1.wsdl"/>
</proxy>
</definitions>

 

Start WSO2 ESB with this configuration by starting WSO2 ESB server and logging in to the ESB console. Go to the Configuration page and replace existing configuration with the above configuration and click Update.

Deploy the SimpleStockQuoteService to sample Axis2 server. Start the sample Axis2 server. For further instructions, see ESB Samples Guide.

Run the client, as given in Sample 150. Try giving differernt symbols such as -Dsymbol=ABC etc.

ant stockquote -Daddurl=https://localhost:8080/soap/StockQuoteProxy -Dsymbol=IBM

You will see log entries similar to the following on the ESB console:

2007-11-22 10:03:02,232 [127.0.1.1-upul] [HttpClientWorker-3]  INFO SurchargeStockQuoteMediator symbol:IBM original price:152.11870457914713 surcharge:15.0% new price:174.9365102660192
2007-11-22 10:02:26,644 [127.0.1.1-upul] [HttpClientWorker-1] INFO SurchargeStockQuoteMediator symbol:MSFT original price:152.6138914900887 surcharge:20.0% new price:183.13666978810642
2007-11-22 10:03:17,262 [127.0.1.1-upul] [HttpClientWorker-4] INFO SurchargeStockQuoteMediator symbol:SUN original price:55.753095784294295 surcharge:25.0% new price:69.69136973036787
2007-11-22 10:02:46,933 [127.0.1.1-upul] [HttpClientWorker-2] INFO SurchargeStockQuoteMediator symbol:QWE original price:93.41435075068514 surcharge:10.0% new price:102.75578582575366

 

Resources

Read Part 1 of this series Writing a Mediator in WSO2 ESB - Part I to learn how to write a simple mediator using the class extension mediator.

For further details on writing mediators, see Extending Synapse in ESB documentation.

Source code: surcharge-stock-quote-mediator-2.zip

 

Author

Upul Godage, Senior Software Engineer, WSO2 Inc. upul AT wso2 DOT com

 

About Author

  • Upul G
  • IT