Opensearch with RestTemplate and JSON

We’re going to revisit OpenSearch in this post using Spring’s RestTemplate API. As described in an earlier post OpenSearch is an open search service specification.

RestTemplate is a flexible API for consuming RESTful web services. In the following example program we consume a search service that returns JSON-formatted results. Comments are inline:

package com.gosynaptic.search;

import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Consume an opensearch service using RestTemplate.
 */
public class OpenSearchClient {
	
	// convert xml document to DOM tree
	private static Document toDomTree(final String xml)
			throws ParserConfigurationException, SAXException, IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();
		Document doc = builder.parse(new InputSource(new StringReader(xml)));
		return doc;
	}
	
	// instantiate an XPath expression
	private static XPathExpression toXPath(final String xp) throws XPathExpressionException {
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		XPathExpression expr = xpath.compile(xp);
		return expr;
	}
	
	// extract String-valued xpath from an xml document
	private static String xtract(final String xpath, final String xml) throws ParserConfigurationException, SAXException, IOException, XPathExpressionException {
		Document doc = toDomTree(xml);
		XPathExpression expr = toXPath(xpath);
		return (String) expr.evaluate(doc, XPathConstants.STRING);
	}
	
	// perform an opensearch search
	public static void main(String[] args) throws Exception {
		
		Logger root = Logger.getRootLogger();
		root.setLevel(Level.DEBUG);
		root.addAppender(new ConsoleAppender(
		    new PatternLayout(PatternLayout.TTCC_CONVERSION_PATTERN)));		
		
		String baseUrl = "http://www.nature.com/";
	
		// get base page
		RestTemplate rt = new RestTemplate();
		ResponseEntity<String> resp = rt.getForEntity(baseUrl, String.class);
		String page = resp.getBody();
		
		// extract service descriptor url
		String descriptorUrl = xtract("//link[@type='application/opensearchdescription+xml']/@href", page);
		System.out.println("opensearch descriptor url:  " + descriptorUrl);
		
		// get opensearch description document
		resp = rt.getForEntity(descriptorUrl, String.class);
		String descriptor = resp.getBody();
//		System.out.println("opensearch descriptor: " + descriptor);
		
		// extract url template for search with JSON results
		String template = xtract("//Url[@type='application/json']/@template", descriptor);
		System.out.println("search template: " + template);
		
		// generate map of template parameters for search
		Map<String, String> params = new HashMap<String,String>();
		String[] data = "searchTerms,iron,sru:queryType?,,startIndex?,1,count?,5,sru:sortKeys?,,sru:stylesheet?,,sru,JSON".split(",");
		for (int i = 0; i < data.length - 1; i += 2) {
			params.put(data[i], data[i+1]);
		}
		
//		// get JSON search results as string for debugging purposes
//		resp = rt.getForEntity(template, String.class, params);
//		System.out.println("results: " + resp.getBody());
		
		// set up JSON message conversion
		List<HttpMessageConverter<?>> converters = rt.getMessageConverters();
		converters.add(new MappingJacksonHttpMessageConverter());
		rt.setMessageConverters(converters);
		// perform search and convert JSON results to POJOs
		SearchResults results = rt.getForObject(template, SearchResults.class, params);
		System.out.println("RESULTS\n" + results);

	}	

}

A single RestTemplate instance created in line 77 is used to perform several HTTP requests. RestTemplate provides Java methods to invoke the various HTTP methods. In the (commented-out) line 102 we invoke an HTTP GET and grab the returned HTTP entity body as a Java String. More interesting is line 110 in which we invoke the same HTTP request but magically convert the JSON result into a corresponding structure of Java POJOs. Here are the POJOs involved:

package com.gosynaptic.search;

import java.io.Serializable;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true)
public class SearchResults implements Serializable {
	private static final long serialVersionUID = 111L;

	private String comment;
	private Feed feed;

	public String getComment() {
		return comment;
	}
	public void setComment(String comment) {
		this.comment = comment;
	}

	public Feed getFeed() {
		return feed;
	}
	public void setFeed(Feed feed) {
		this.feed = feed;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder(getClass().getName() + "\n");
		sb.append("\tcomment: " + getComment() + "\n");
		sb.append(getFeed());
		return sb.toString();
	}	

}
package com.gosynaptic.search;

import java.io.Serializable;
import java.util.ArrayList;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true)
public class Feed implements Serializable {
	private static final long serialVersionUID = 222L;

	private String updated;
	private ArrayList entries = new ArrayList();

	public String getUpdated() {
		return updated;
	}
	public void setUpdated(String updated) {
		this.updated = updated;
	}

	public ArrayList getEntry() {
		return entries;
	}
	public void setEntry(ArrayList entries) {
		this.entries = entries;
	}

	public String toString() {
		StringBuilder sb = new StringBuilder(getClass().getName() + "\n");
		sb.append("\tupdated: " + getUpdated() + "\n");
		for (Entry e : getEntry()) {
			sb.append(e.toString());
		}
		return sb.toString();
	}
}
package com.gosynaptic.search;

import java.io.Serializable;

import org.codehaus.jackson.annotate.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown=true)
class Entry implements Serializable {
	private static final long serialVersionUID = 333L;

	private String title;
	private String link;

	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}

	public String getLink() {
		return link;
	}
	public void setLink(String link) {
		this.link = link;
	}

	public String toString() {
		return getClass().getName() + "\n\ttitle: " + getTitle() + "\n\tlink: " + getLink() + "\n";
	}
}

The "JsonIgnoreProperties" annotation on each POJO tells the JSON converter not to worry about JSON properties not found on the similarly-named POJOs.

The program prints the following output derived from the toString() methods in the POJO tree paralleling the JSON:

RESULTS
com.gosynaptic.search.SearchResults
    comment: nature.com OpenSearch: urn:uuid:dee5ef48-f5e7-4102-8c5d-545852682793
com.gosynaptic.search.Feed
    updated: 2012-11-17T18:12:00+00:00
com.gosynaptic.search.Entry
    title: Endodcytic labelling of visceral endoderm of mouse perigastrulation embryos
    link: http://dx.doi.org/10.1038/protex.2012.039
com.gosynaptic.search.Entry
    title: Iron supplementation to treat anemia in patients with chronic kidney disease
    link: http://dx.doi.org/10.1038/nrneph.2010.139
com.gosynaptic.search.Entry
    title: Diagnostic value of iron indices in hemodialysis patients receiving epoetin
    link: http://dx.doi.org/10.1046/j.1523-1755.2001.00800.x
com.gosynaptic.search.Entry
    title: Transcriptome response of high- and low-light-adapted Prochlorococcus strains to changing iron availability
    link: http://dx.doi.org/10.1038/ismej.2011.49
com.gosynaptic.search.Entry
    title: Iron status in patients receiving erythropoietin for dialysis-associated anemia
    link: http://dx.doi.org/10.1038/ki.1989.43

We didn’t have to worry about HTTP headers in this example but if you needed to set the Accept header, for example, you could do it like this:

	// interceptor used to set accept headers on requests
    private static class AcceptHeaderInterceptor implements ClientHttpRequestInterceptor {
        private String header;

        public AcceptHeaderInterceptor(final String acceptHeader) {
            header = acceptHeader;
        }

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                ClientHttpRequestExecution execution) throws IOException {
            HttpRequestWrapper wrapper = new HttpRequestWrapper(request);
            wrapper.getHeaders().setAccept(MediaType.parseMediaTypes(header));
            return execution.execute(wrapper, body);
        }
    }

    RestTemplate rt = new RestTemplate();
    rt.setInterceptors(Collections.singletonList(
        new AcceptHeaderInterceptor("application/json,text/xml,text/xhtml+xml,text/plain")));

If you need to access an SSL-protected service with RestTemplate here’s a serving suggestion:

    private static ClientHttpRequestFactory createClientHttpRequestFactory(
    		String keyStorePath,
    		String keyStorePassword,
    		String trustStorePath,
    		String trustStorePassword
    		) throws GeneralSecurityException, IOException, IOException {
    	KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    	ks.load(new FileInputStream(keyStorePath), keyStorePassword.toCharArray());
    	KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
    	ts.load(new FileInputStream(trustStorePath), trustStorePassword.toCharArray());
    	SSLSocketFactory sf = new SSLSocketFactory(SSLSocketFactory.TLS, ks, keyStorePassword,
    			ts, new SecureRandom(), SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
    	Scheme scheme = new Scheme("https", 443, sf);
    	PoolingClientConnectionManager cm = new PoolingClientConnectionManager();
    	cm.setDefaultMaxPerRoute(10);
    	cm.getSchemeRegistry().register(scheme);
    	DefaultHttpClient client = new DefaultHttpClient(cm);
    	return new HttpComponentsClientHttpRequestFactory(client);
    }

    RestTemplate rt = new RestTemplate();
    rt.setRequestFactory(createClientHttpRequestFactory("/path/to/ks.jks", "ksPwd", "/path/to/ts.jks", "tsPwd"));

And you can control error handling with the RestTemplate.setErrorHandler() method.

Note that we used Spring 3.1.2, Apache HttpClient 4.2.1, and Jackson 1.9.11 in this example.

Posted in Technical | Leave a comment