package org.apache.solr.client; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.apache.solr.util.XML; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; /** * This class is for use in clients of the Solr webapp. Specifically, * it implements a set of methods that allow add, delete, commit and * optimize messages to be sent to the server without the caller having * to format XML messages. The APIs are exposed as normal java methods * and the XML generation and communication with the server happen behind * the scenes. *

* Usage is quite simple. Just construct an instance with the URL of the * server you want to run against, then start calling add(), * delete() and so on. The URL you use at construction time * is typically something like "http://localhost:8983/solr/update" * if you are using the standard distribution Solr webapp on your local machine. * Obviously you will want to change this to hit other hosts as your needs * dictate. *

* Documents are represented as simple maps from String to * Object. If an object is multivalued, the value in the map * can be either an array or a collection. The add code will iterate over * all of the values and insert them correctly. * * @author vengroff */ public class DocumentManagerClient { /** * The URL of the Solr server. */ private URL solrServerUrl; /** * Factory for parsers for the XML we get back from the server. */ XmlPullParserFactory factory; /** * This interface is for objects that write XML requests to the * server. Normally, these objects are created on the fly and * passed to ClientStub.serverCall(RequestWriter). * All the public APIs that make calls to the server do so * by constructing one of these and then passing it to * serverCall(RequestWriter). * @see DocumentManagerClient#serverCall(RequestWriter) */ protected static interface RequestWriter { /** * Write an XML request for a Solr server to the given writer. * @param writer The writer to write the XML to. * @throws IOException if writing fails. */ void writeRequest(Writer writer) throws IOException; }; /** * An implementation of the RequestWriter interface that writes * a constant. Useful for e.g. commit and optimize messages. */ protected static class ConstantRequestWriter implements RequestWriter { private final String constant; /** * @param constant */ public ConstantRequestWriter(String constant) { this.constant = constant; } public void writeRequest(Writer writer) throws IOException { writer.write(constant); } } /** * @param solrServerUrl The URL of the Solr server. For * example, "http://localhost:8983/solr/update" * if you are using the standard distribution Solr webapp * on your local machine. * @throws SolrClientException If unable to construct, typically * due to some problem with the XPP parser factory. */ public DocumentManagerClient(URL solrServerUrl) throws SolrClientException { this.solrServerUrl = solrServerUrl; try { factory = XmlPullParserFactory.newInstance(); } catch (XmlPullParserException e) { throw new SolrClientException("Unable to get XPP factory instance", e); } factory.setNamespaceAware(false); } /** * This is the meat of any call to the server. The specifics of what the call * does are controlled by the RequestWriter that is passed in. * @param requestWriter * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. * @see RequestWriter */ protected void serverCall(RequestWriter requestWriter) throws SolrClientException, SolrServerException { try { URLConnection connection = solrServerUrl.openConnection(); connection.setDoOutput(true); OutputStream outputStream = connection.getOutputStream(); try { Writer writer = new OutputStreamWriter(outputStream); try { requestWriter.writeRequest(writer); } finally { writer.close(); } } finally { outputStream.close(); } InputStream inputStream = connection.getInputStream(); try { Reader reader = new InputStreamReader(inputStream); try { XmlPullParser xpp; try { xpp = factory.newPullParser(); xpp.setInput(reader); xpp.nextTag(); } catch(XmlPullParserException e) { throw new SolrClientException("XML parsing exception in solr client", e); } if(!"result".equals(xpp.getName())) { throw new SolrClientException("Result from server is not rooted with a tag."); } String statusString = xpp.getAttributeValue(null, "status"); int status = Integer.parseInt(statusString); if(status != 0) { try { throw new SolrServerException("Server returned non-zero status: ", status, xpp.nextText()); } catch(XmlPullParserException e) { throw new SolrClientException("XML parsing exception in solr client", e); } } } finally { reader.close(); } } finally { inputStream.close(); } } catch (IOException e) { throw new SolrClientException("I/O exception in solr client", e); } } /** * Add a document to the index. A document is simply a map from field names * to field values. The field values can be of any type. toString() * is called on them to get strings to use as values in the XML sent to the server. *

* We could have made a little document class to use as a parameter here, but it * would have ended up just containing a map. Callers of this API are likely to * be getting their data in any number of forms, so at least with the map * representation, a non-zero percentage of them will be able to add documents * without constructing objects of a special new class. * @param document A document. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. * @see #add(Collection) */ public void add(Map document) throws SolrClientException, SolrServerException { Collection> documents = new ArrayList>(1); documents.add(document); add(documents); } /** * Add a collection of documents to the index. Each document in the * collection is simply a map from field names to field values. The * field values can be of any type. If the value for a given field * is neither a Collection or an array, we call * toString() on it to get strings to use as a value in * the XML sent to the server. If a given field has a value that is a * Collection or an array, we assume it is a multivalued * attribute and iterate over the elements of the collection or array, * inserting each as a seperate value for the field. * @param documents Collection of documents. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. * @see #add(Map) */ public void add(final Collection> documents) throws SolrClientException, SolrServerException { // The strategy here is to just build a custom request writer // that knows how to write an block covering the whole // collection of docs. RequestWriter requestWriter = new RequestWriter() { /** * Helper funtion to write a field and its value to our writer. */ private void writeFieldValue(Writer writer, String fieldName, Object fieldValue) throws IOException { XML.writeXML(writer, "field", fieldValue.toString(), "name", fieldName); } /** * Write an add block for the collection of documents we are given. * @see RequestWriter#writeRequest(Writer) */ public void writeRequest(Writer writer) throws IOException { writer.write(""); for(Map document : documents) { writer.write(""); for(Map.Entry field : document.entrySet()) { String fieldName = field.getKey(); Object fieldValue = field.getValue(); if(fieldValue instanceof Collection) { // Iterate over a collection of values, writing each one. for(Object individualFieldValue : (Collection)fieldValue) { writeFieldValue(writer, fieldName, individualFieldValue); } } else if(fieldValue.getClass().isArray()) { // Iterate over an array, writing each value. for(Object individualFieldValue : (Object[])fieldValue) { writeFieldValue(writer, fieldName, individualFieldValue); } } else { // Single value. Just write it. writeFieldValue(writer, fieldName, fieldValue); } } writer.write(""); } writer.write(""); } }; serverCall(requestWriter); } /** * Delete a document with the given id. * @param id The document id to delete. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. */ public void delete(final String id) throws SolrClientException, SolrServerException { // The strategy here is to just build a custom request writer // that knows how to write a block for the id we // were given. RequestWriter requestWriter = new RequestWriter() { public void writeRequest(Writer writer) throws IOException { writer.write(""); XML.escapeCharData(id, writer); writer.write(""); } }; serverCall(requestWriter); } /** * Delete documents found by a given query. * @param query The query for documents to delete. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. */ public void deleteByQuery(final String query) throws SolrClientException, SolrServerException { // The strategy here is to just build a custom request writer // that knows how to write a block for the query we // were given. RequestWriter requestWriter = new RequestWriter() { public void writeRequest(Writer writer) throws IOException { writer.write(""); XML.escapeCharData(query, writer); writer.write(""); } }; serverCall(requestWriter); } /** * Send a commit request to the server. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. */ public void commit() throws SolrClientException, SolrServerException { serverCall(new ConstantRequestWriter("")); } /** * Send an optimize request to the server. * @throws SolrClientException if there is a client-side problem. * @throws SolrServerException if there is a server-side problem. */ public void optimize() throws SolrClientException, SolrServerException { serverCall(new ConstantRequestWriter("")); } }