/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.solr.util; import org.apache.solr.common.params.CommonParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.common.util.XML; import org.apache.solr.core.SolrConfig; import org.apache.solr.core.SolrCore; import org.apache.solr.core.CoreContainer; import org.apache.solr.core.CoreDescriptor; import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.handler.XmlUpdateRequestHandler; import org.apache.solr.request.LocalSolrQueryRequest; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.response.QueryResponseWriter; import org.apache.solr.response.SolrQueryResponse; import org.apache.solr.schema.IndexSchema; import org.w3c.dom.Document; import org.xml.sax.SAXException; import org.apache.solr.common.util.NamedList.NamedListEntry; 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.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This class provides a simple harness that may be useful when * writing testcases. * * <p> * This class lives in the main source tree (and not in the test source * tree), so that it will be included with even the most minimal solr * distribution, in order to encourage plugin writers to create unit * tests for their plugins. * * @version $Id:$ */ public class TestHarness { protected CoreContainer container; private SolrCore core; private XPath xpath = XPathFactory.newInstance().newXPath(); private DocumentBuilder builder; public XmlUpdateRequestHandler updater; public static SolrConfig createConfig(String confFile) { // set some system properties for use by tests System.setProperty("solr.test.sys.prop1", "propone"); System.setProperty("solr.test.sys.prop2", "proptwo"); try { return new SolrConfig(confFile); } catch(Exception xany) { throw new RuntimeException(xany); } } /** * Assumes "solrconfig.xml" is the config file to use, and * "schema.xml" is the schema path to use. * * @param dataDirectory path for index data, will not be cleaned up */ public TestHarness( String dataDirectory) { this( dataDirectory, "schema.xml"); } /** * Assumes "solrconfig.xml" is the config file to use. * * @param dataDirectory path for index data, will not be cleaned up * @param schemaFile path of schema file */ public TestHarness( String dataDirectory, String schemaFile) { this( dataDirectory, "solrconfig.xml", schemaFile); } /** * @param dataDirectory path for index data, will not be cleaned up * @param configFile solrconfig filename * @param schemaFile schema filename */ public TestHarness( String dataDirectory, String configFile, String schemaFile) { this( dataDirectory, createConfig(configFile), schemaFile); } /** * @param dataDirectory path for index data, will not be cleaned up * @param solrConfig solronfig instance * @param schemaFile schema filename */ public TestHarness( String dataDirectory, SolrConfig solrConfig, String schemaFile) { this( dataDirectory, solrConfig, new IndexSchema(solrConfig, schemaFile, null)); } /** * @param dataDirectory path for index data, will not be cleaned up * @param solrConfig solrconfig instance * @param indexSchema schema instance */ public TestHarness( String dataDirectory, SolrConfig solrConfig, IndexSchema indexSchema) { this("", new Initializer("", dataDirectory, solrConfig, indexSchema)); } public TestHarness(String coreName, CoreContainer.Initializer init) { try { container = init.initialize(); if (coreName == null) coreName = ""; // get the core & decrease its refcount: // the container holds the core for the harness lifetime core = container.getCore(coreName); if (core != null) core.close(); builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); updater = new XmlUpdateRequestHandler(); updater.init( null ); } catch (Exception e) { throw new RuntimeException(e); } } // Creates a container based on infos needed to create one core static class Initializer extends CoreContainer.Initializer { String coreName; String dataDirectory; SolrConfig solrConfig; IndexSchema indexSchema; public Initializer(String coreName, String dataDirectory, SolrConfig solrConfig, IndexSchema indexSchema) { if (coreName == null) coreName = ""; this.coreName = coreName; this.dataDirectory = dataDirectory; this.solrConfig = solrConfig; this.indexSchema = indexSchema; } public String getCoreName() { return coreName; } @Override public CoreContainer initialize() { CoreContainer container = new CoreContainer(new SolrResourceLoader(SolrResourceLoader.locateSolrHome())); CoreDescriptor dcore = new CoreDescriptor(container, coreName, solrConfig.getResourceLoader().getInstanceDir()); dcore.setConfigName(solrConfig.getResourceName()); dcore.setSchemaName(indexSchema.getResourceName()); SolrCore core = new SolrCore( null, dataDirectory, solrConfig, indexSchema, dcore); container.register(coreName, core, false); return container; } } public CoreContainer getCoreContainer() { return container; } public SolrCore getCore() { return core; } /** * Processes an "update" (add, commit or optimize) and * returns the response as a String. * * @deprecated The better approach is to instantiate an Updatehandler directly * * @param xml The XML of the update * @return The XML response to the update */ @Deprecated public String update(String xml) { StringReader req = new StringReader(xml); StringWriter writer = new StringWriter(32000); updater.doLegacyUpdate(req, writer); return writer.toString(); } /** * Validates that an "update" (add, commit or optimize) results in success. * * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved * * @param xml The XML of the update * @return null if successful, otherwise the XML response to the update */ public String validateUpdate(String xml) throws SAXException { return checkUpdateStatus(xml, "0"); } /** * Validates that an "update" (add, commit or optimize) results in success. * * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved * * @param xml The XML of the update * @return null if successful, otherwise the XML response to the update */ public String validateErrorUpdate(String xml) throws SAXException { return checkUpdateStatus(xml, "1"); } /** * Validates that an "update" (add, commit or optimize) results in success. * * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved * * @param xml The XML of the update * @return null if successful, otherwise the XML response to the update */ public String checkUpdateStatus(String xml, String code) throws SAXException { try { String res = update(xml); String valid = validateXPath(res, "//result[@status="+code+"]" ); return (null == valid) ? null : res; } catch (XPathExpressionException e) { throw new RuntimeException ("?!? static xpath has bug?", e); } } /** * Validates that an add of a single document results in success. * * @param fieldsAndValues Odds are field names, Evens are values * @return null if successful, otherwise the XML response to the update * @see #appendSimpleDoc */ public String validateAddDoc(String... fieldsAndValues) throws XPathExpressionException, SAXException, IOException { StringBuilder buf = new StringBuilder(); buf.append("<add>"); appendSimpleDoc(buf, fieldsAndValues); buf.append("</add>"); String res = update(buf.toString()); String valid = validateXPath(res, "//result[@status=0]" ); return (null == valid) ? null : res; } /** * Validates a "query" response against an array of XPath test strings * * @param req the Query to process * @return null if all good, otherwise the first test that fails. * @exception Exception any exception in the response. * @exception IOException if there is a problem writing the XML * @see LocalSolrQueryRequest */ public String validateQuery(SolrQueryRequest req, String... tests) throws IOException, Exception { String res = query(req); return validateXPath(res, tests); } /** * Processes a "query" using a user constructed SolrQueryRequest * * @param req the Query to process, will be closed. * @return The XML response to the query * @exception Exception any exception in the response. * @exception IOException if there is a problem writing the XML * @see LocalSolrQueryRequest */ public String query(SolrQueryRequest req) throws IOException, Exception { return query(req.getParams().get(CommonParams.QT), req); } /** * Processes a "query" using a user constructed SolrQueryRequest * * @param handler the name of the request handler to process the request * @param req the Query to process, will be closed. * @return The XML response to the query * @exception Exception any exception in the response. * @exception IOException if there is a problem writing the XML * @see LocalSolrQueryRequest */ public String query(String handler, SolrQueryRequest req) throws IOException, Exception { SolrQueryResponse rsp = queryAndResponse(handler, req); StringWriter sw = new StringWriter(32000); QueryResponseWriter responseWriter = core.getQueryResponseWriter(req); responseWriter.write(sw,req,rsp); req.close(); return sw.toString(); } public SolrQueryResponse queryAndResponse(String handler, SolrQueryRequest req) throws Exception { SolrQueryResponse rsp = new SolrQueryResponse(); core.execute(core.getRequestHandler(handler),req,rsp); if (rsp.getException() != null) { throw rsp.getException(); } return rsp; } /** * A helper method which valides a String against an array of XPath test * strings. * * @param xml The xml String to validate * @param tests Array of XPath strings to test (in boolean mode) on the xml * @return null if all good, otherwise the first test that fails. */ public String validateXPath(String xml, String... tests) throws XPathExpressionException, SAXException { if (tests==null || tests.length == 0) return null; Document document=null; try { document = builder.parse(new ByteArrayInputStream (xml.getBytes("UTF-8"))); } catch (UnsupportedEncodingException e1) { throw new RuntimeException("Totally weird UTF-8 exception", e1); } catch (IOException e2) { throw new RuntimeException("Totally weird io exception", e2); } for (String xp : tests) { xp=xp.trim(); Boolean bool = (Boolean) xpath.evaluate(xp, document, XPathConstants.BOOLEAN); if (!bool) { return xp; } } return null; } /** * Shuts down and frees any resources */ public void close() { if (container != null) { for (SolrCore c : container.getCores()) { if (c.getOpenCount() > 1) throw new RuntimeException("SolrCore.getOpenCount()=="+core.getOpenCount()); } } if (container != null) { container.shutdown(); container = null; } } /** * A helper that adds an xml <doc> containing all of the * fields and values specified (odds are fields, evens are values) * to a StringBuilder */ public void appendSimpleDoc(StringBuilder buf, String... fieldsAndValues) throws IOException { buf.append(makeSimpleDoc(fieldsAndValues)); } /** * A helper that adds an xml <doc> containing all of the * fields and values specified (odds are fields, evens are values) * to a StringBuffer. * @deprecated see {@link #appendSimpleDoc(StringBuilder, String...)} */ public void appendSimpleDoc(StringBuffer buf, String... fieldsAndValues) throws IOException { buf.append(makeSimpleDoc(fieldsAndValues)); } /** * A helper that creates an xml <doc> containing all of the * fields and values specified * * @param fieldsAndValues 0 and Even numbered args are fields names odds are field values. */ public static StringBuffer makeSimpleDoc(String... fieldsAndValues) { try { StringWriter w = new StringWriter(); w.append("<doc>"); for (int i = 0; i < fieldsAndValues.length; i+=2) { XML.writeXML(w, "field", fieldsAndValues[i+1], "name", fieldsAndValues[i]); } w.append("</doc>"); return w.getBuffer(); } catch (IOException e) { throw new RuntimeException ("this should never happen with a StringWriter", e); } } /** * Generates a delete by query xml string * @param q Query that has not already been xml escaped */ public static String deleteByQuery(String q) { return delete("query", q); } /** * Generates a delete by id xml string * @param id ID that has not already been xml escaped */ public static String deleteById(String id) { return delete("id", id); } /** * Generates a delete xml string * @param val text that has not already been xml escaped */ private static String delete(String deltype, String val) { try { StringWriter r = new StringWriter(); r.write("<delete>"); XML.writeXML(r, deltype, val); r.write("</delete>"); return r.getBuffer().toString(); } catch (IOException e) { throw new RuntimeException ("this should never happen with a StringWriter", e); } } /** * Helper that returns an <optimize> String with * optional key/val pairs. * * @param args 0 and Even numbered args are params, Odd numbered args are values. */ public static String optimize(String... args) { return simpleTag("optimize", args); } private static String simpleTag(String tag, String... args) { try { StringWriter r = new StringWriter(); // this is annoying if (null == args || 0 == args.length) { XML.writeXML(r, tag, null); } else { XML.writeXML(r, tag, null, (Object[])args); } return r.getBuffer().toString(); } catch (IOException e) { throw new RuntimeException ("this should never happen with a StringWriter", e); } } /** * Helper that returns an <commit> String with * optional key/val pairs. * * @param args 0 and Even numbered args are params, Odd numbered args are values. */ public static String commit(String... args) { return simpleTag("commit", args); } public LocalRequestFactory getRequestFactory(String qtype, int start, int limit) { LocalRequestFactory f = new LocalRequestFactory(); f.qtype = qtype; f.start = start; f.limit = limit; return f; } /** * 0 and Even numbered args are keys, Odd numbered args are values. */ public LocalRequestFactory getRequestFactory(String qtype, int start, int limit, String... args) { LocalRequestFactory f = getRequestFactory(qtype, start, limit); for (int i = 0; i < args.length; i+=2) { f.args.put(args[i], args[i+1]); } return f; } public LocalRequestFactory getRequestFactory(String qtype, int start, int limit, Map<String,String> args) { LocalRequestFactory f = getRequestFactory(qtype, start, limit); f.args.putAll(args); return f; } /** * A Factory that generates LocalSolrQueryRequest objects using a * specified set of default options. */ public class LocalRequestFactory { public String qtype = "standard"; public int start = 0; public int limit = 1000; public Map<String,String> args = new HashMap<String,String>(); public LocalRequestFactory() { } /** * Creates a LocalSolrQueryRequest based on variable args; for * historical reasons, this method has some peculiar behavior: * <ul> * <li>If there is a single arg, then it is treated as the "q" * param, and the LocalSolrQueryRequest consists of that query * string along with "qt", "start", and "rows" params (based * on the qtype, start, and limit properties of this factory) * along with any other default "args" set on this factory. * </li> * <li>If there are multiple args, then there must be an even number * of them, and each pair of args is used as a key=value param in * the LocalSolrQueryRequest. <b>NOTE: In this usage, the "qtype", * "start", "limit", and "args" properties of this factory are * ignored.</b> * </li> * </ul> */ public LocalSolrQueryRequest makeRequest(String ... q) { if (q.length==1) { return new LocalSolrQueryRequest(TestHarness.this.getCore(), q[0], qtype, start, limit, args); } if (q.length%2 != 0) { throw new RuntimeException("The length of the string array (query arguments) needs to be even"); } Map.Entry<String, String> [] entries = new NamedListEntry[q.length / 2]; for (int i = 0; i < q.length; i += 2) { entries[i/2] = new NamedListEntry<String>(q[i], q[i+1]); } return new LocalSolrQueryRequest(TestHarness.this.getCore(), new NamedList(entries)); } } }