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.


