Tapestry - Part IV: Extended API using XML

Introduction

In a previous blog in our Tapestry series we showed that Tapestry can be used as an API. While we would advice to do this only for simple cases, it could be a good solution if you wanted to offer Web API for your existing Tapestry application. If you were interested in a fully-fledged Web API with all the bells and whistles it would probably be better to use a separate framework.

In a previous blog we build an API returning JSON. This raises the question: is it also possible to return XML? The answer turns out to be yes!

In this blog we will show how to do so.

Prerequisites

Setup

Grab the code hello_tapestry_validation from the previous blog and use it as starting point. Import that project into Eclipse and run it with the following configuration:

run configuration - main

and

run configuration - JRE

If everything is configured properly, you will get the basic hello-world page:

final result

(If you are running a JDK < 1.8 you should also add -XX:MaxPermSize=256M to JRE config.).

Now that everything is setup, we can start extending the API to allow for interaction using JSON and XML.

Adding TextStreamResponse Responders

In the previous blog we got things to work, but if we keep adding responders to the Api class things will get very messy. Instead we will introduce Responders that render to result of the hello tapestry application to the correct format.

package nl.uglyduckling.hello.responder;

import java.util.Date;

public abstract class ResponderBase implements ApiResponder{

	protected final String version;
	protected final Date date;

	public ResponderBase(String version, Date date) {
		this.version = version;
		this.date = date;
	}
}

Add this interface to the project. Be sure not to stick it in one of the default packages created by the Tapestry Skeleton project as this will create issues. Now let's refactor the code so that the JSON response is created using an object.

package nl.uglyduckling.hello.responder;


import java.util.Date;

import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.util.TextStreamResponse;

public class JsonResponder extends ResponderBase {

	public JsonResponder(String version, Date date) {
		super(version, date);
	}

	public TextStreamResponse Render(String userName) {
		JSONObject json = getJsonResult(userName);

		return new TextStreamResponse("application/json", json.toCompactString()); 
	}

	private JSONObject getJsonResult(String userName) {
		JSONObject json = new JSONObject();

		json.put("result", "Hello " + userName);
		json.put("version", version);
		json.put("timestamp", date.toString());
		return json;
	}

}

Using the base class

package nl.uglyduckling.hello.responder;

import java.util.Date;

public abstract class ResponderBase implements ApiResponder{

	protected final String version;
	protected final Date date;

	public ResponderBase(String version, Date date) {
		this.version = version;
		this.date = date;
	}

}

with some small fixes in the Api object you should now get a result like this (again).

api result fred

Now we are going to add a second parameter to the onActivate method in Api. This parameter will indicate weather to return JSON or XML.

package nl.uglyduckling.hello.pages;


import nl.uglyduckling.hello.responder.ApiResponder;
import nl.uglyduckling.hello.responder.JsonResponder;
import nl.uglyduckling.hello.responder.XmlResponder;

import java.util.Date;

import org.apache.tapestry5.StreamResponse;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.util.TextStreamResponse;
import org.slf4j.Logger;

public class Api {
	@Property
	@Inject
	@Symbol(SymbolConstants.APPLICATION_VERSION)
	private String version;

	@Inject
	private Logger logger;

	private final String TYPE_JSON = "JSON";
	private final String TYPE_XML = "XML";

	StreamResponse onActivate(String type, String userName)
	{
		ApiResponder apiResponder;
		Date date = new Date();
		if (type.compareToIgnoreCase(TYPE_JSON) == 0)
		{
			apiResponder = new JsonResponder(version, date);
		}
		else if(type.compareToIgnoreCase(TYPE_XML) == 0)
		{
			apiResponder = new XmlResponder(version, date);
		}
		else
		{
			return new TextStreamResponse("plain/text", "first parameter must be XML or JSON. "
					+ "Check http:\\\\localhost:8080\\hello\\api for the online documentation."); 
		}

		return apiResponder.Render(userName);
	}
}

The last step is to create an XML responder to give us back the XML-formatted string. We implemented it in the following way

package nl.uglyduckling.hello.responder;

import java.io.StringWriter;
import java.util.Date;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.tapestry5.util.TextStreamResponse;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

public class XmlResponder extends ResponderBase {

	
	public XmlResponder(String version, Date date) {
		super(version, date);
	}

	@Override
	public TextStreamResponse Render(String userName) 
	{
		try
		{
			String xml = getXmlDocumentResult(userName);
			return new TextStreamResponse("application/xml", xml); 
		}
		catch (Exception e) {
			return new TextStreamResponse("plain/text", "Internal error " + e.getMessage()); 
		}
	}
	
	private String getXmlDocumentResult(String userName) 
			throws ParserConfigurationException, TransformerFactoryConfigurationError, TransformerException 
	{
		DocumentBuilderFactory icFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder icBuilder;

		icBuilder = icFactory.newDocumentBuilder();
		Document doc = icBuilder.newDocument();
		Element mainRootElement = doc.createElementNS("http://uglyduckling.nl/CreateXMLDOM", "HelloWorld");
		doc.appendChild(mainRootElement);

		mainRootElement.appendChild(buildXmlNode("result", "Hello " + userName, doc));
		mainRootElement.appendChild(buildXmlNode("version", version, doc));
		mainRootElement.appendChild(buildXmlNode("timestamp", date.toString(), doc));

		String finalstring = documentToString(doc);

		return finalstring;
	}

	private String documentToString(Document doc)
			throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException {
		Transformer transformer = TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.INDENT, "yes"); 
		DOMSource source = new DOMSource(doc);
		StringWriter outWriter = new StringWriter();
		StreamResult result = new StreamResult( outWriter );
		transformer.transform(source, result);
		StringBuffer sb = outWriter.getBuffer(); 
		String finalstring = sb.toString();
		return finalstring;
	}

	private Node buildXmlNode(String name, String value, Document dom)
	{
		Element node = dom.createElement(name);
		node.appendChild(dom.createTextNode(value));
		return node;
	}
}

Calling the url which ends with hello/api/xml/fred will give you:

fred xml result

If you are using Safari the XML may look a little funny. Please refer to this blog to see how you can configure Safari in just two steps to get nicely-formatted XML.

Code

The code is available here : hello_tapestry_xml_api

 

Leave a Reply

Your email address will not be published. Required fields are marked *