001: /**
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */package org.apache.solr.util;
017:
018: import org.apache.solr.core.SolrConfig;
019: import org.apache.solr.core.SolrCore;
020: import org.apache.solr.handler.XmlUpdateRequestHandler;
021: import org.apache.solr.request.LocalSolrQueryRequest;
022: import org.apache.solr.request.QueryResponseWriter;
023: import org.apache.solr.request.SolrQueryRequest;
024: import org.apache.solr.request.SolrQueryResponse;
025: import org.apache.solr.schema.IndexSchema;
026: import org.w3c.dom.Document;
027: import org.xml.sax.SAXException;
028:
029: import javax.xml.parsers.DocumentBuilder;
030: import javax.xml.parsers.DocumentBuilderFactory;
031: import javax.xml.xpath.XPath;
032: import javax.xml.xpath.XPathConstants;
033: import javax.xml.xpath.XPathExpressionException;
034: import javax.xml.xpath.XPathFactory;
035:
036: import java.io.ByteArrayInputStream;
037: import java.io.IOException;
038: import java.io.PrintWriter;
039: import java.io.Reader;
040: import java.io.StringReader;
041: import java.io.StringWriter;
042: import java.io.UnsupportedEncodingException;
043: import java.io.Writer;
044: import java.util.Arrays;
045: import java.util.HashMap;
046: import java.util.Map;
047:
048: /**
049: * This class provides a simple harness that may be useful when
050: * writing testcases.
051: *
052: * <p>
053: * This class lives in the main source tree (and not in the test source
054: * tree), so that it will be included with even the most minimal solr
055: * distribution, in order to encourage plugin writers to create unit
056: * tests for their plugins.
057: *
058: * @author hossman
059: * @version $Id:$
060: */
061: public class TestHarness {
062:
063: private SolrCore core;
064: private XPath xpath = XPathFactory.newInstance().newXPath();
065: private DocumentBuilder builder;
066: XmlUpdateRequestHandler updater;
067:
068: /**
069: * Assumes "solrconfig.xml" is the config file to use, and
070: * "schema.xml" is the schema path to use.
071: *
072: * @param dataDirectory path for index data, will not be cleaned up
073: */
074: public TestHarness(String dataDirectory) {
075: this (dataDirectory, "schema.xml");
076: }
077:
078: /**
079: * Assumes "solrconfig.xml" is the config file to use.
080: *
081: * @param dataDirectory path for index data, will not be cleaned up
082: * @param schemaFile path of schema file
083: */
084: public TestHarness(String dataDirectory, String schemaFile) {
085: this (dataDirectory, "solrconfig.xml", schemaFile);
086: }
087:
088: /**
089: * @param dataDirectory path for index data, will not be cleaned up
090: * @param confFile solrconfig filename
091: * @param schemaFile schema filename
092: */
093: public TestHarness(String dataDirectory, String confFile,
094: String schemaFile) {
095: try {
096: SolrConfig.initConfig(confFile);
097: core = new SolrCore(dataDirectory, new IndexSchema(
098: schemaFile));
099: builder = DocumentBuilderFactory.newInstance()
100: .newDocumentBuilder();
101:
102: updater = new XmlUpdateRequestHandler();
103: updater.init(null);
104: } catch (Exception e) {
105: throw new RuntimeException(e);
106: }
107: }
108:
109: /**
110: * Processes an "update" (add, commit or optimize) and
111: * returns the response as a String.
112: *
113: * The better approach is to instanciate a Updatehandler directly
114: *
115: * @param xml The XML of the update
116: * @return The XML response to the update
117: */
118: @Deprecated
119: public String update(String xml) {
120:
121: StringReader req = new StringReader(xml);
122: StringWriter writer = new StringWriter(32000);
123:
124: updater.doLegacyUpdate(req, writer);
125:
126: return writer.toString();
127: }
128:
129: /**
130: * Validates that an "update" (add, commit or optimize) results in success.
131: *
132: * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved
133: *
134: * @param xml The XML of the update
135: * @return null if succesful, otherwise the XML response to the update
136: */
137: public String validateUpdate(String xml) throws SAXException {
138: return checkUpdateStatus(xml, "0");
139: }
140:
141: /**
142: * Validates that an "update" (add, commit or optimize) results in success.
143: *
144: * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved
145: *
146: * @param xml The XML of the update
147: * @return null if succesful, otherwise the XML response to the update
148: */
149: public String validateErrorUpdate(String xml) throws SAXException {
150: return checkUpdateStatus(xml, "1");
151: }
152:
153: /**
154: * Validates that an "update" (add, commit or optimize) results in success.
155: *
156: * :TODO: currently only deals with one add/doc at a time, this will need changed if/when SOLR-2 is resolved
157: *
158: * @param xml The XML of the update
159: * @return null if succesful, otherwise the XML response to the update
160: */
161: public String checkUpdateStatus(String xml, String code)
162: throws SAXException {
163: try {
164: String res = update(xml);
165: String valid = validateXPath(res, "//result[@status="
166: + code + "]");
167: return (null == valid) ? null : res;
168: } catch (XPathExpressionException e) {
169: throw new RuntimeException("?!? static xpath has bug?", e);
170: }
171: }
172:
173: /**
174: * Validates that an add of a single document results in success.
175: *
176: * @param fieldsAndValues Odds are field names, Evens are values
177: * @return null if succesful, otherwise the XML response to the update
178: * @see #appendSimpleDoc
179: */
180: public String validateAddDoc(String... fieldsAndValues)
181: throws XPathExpressionException, SAXException, IOException {
182:
183: StringBuffer buf = new StringBuffer();
184: buf.append("<add>");
185: appendSimpleDoc(buf, fieldsAndValues);
186: buf.append("</add>");
187:
188: String res = update(buf.toString());
189: String valid = validateXPath(res, "//result[@status=0]");
190: return (null == valid) ? null : res;
191: }
192:
193: /**
194: * Validates a "query" response against an array of XPath test strings
195: *
196: * @param req the Query to process
197: * @return null if all good, otherwise the first test that fails.
198: * @exception Exception any exception in the response.
199: * @exception IOException if there is a problem writing the XML
200: * @see LocalSolrQueryRequest
201: */
202: public String validateQuery(SolrQueryRequest req, String... tests)
203: throws IOException, Exception {
204:
205: String res = query(req);
206: return validateXPath(res, tests);
207: }
208:
209: /**
210: * Processes a "query" using a user constructed SolrQueryRequest
211: *
212: * @param req the Query to process, will be closed.
213: * @return The XML response to the query
214: * @exception Exception any exception in the response.
215: * @exception IOException if there is a problem writing the XML
216: * @see LocalSolrQueryRequest
217: */
218: public String query(SolrQueryRequest req) throws IOException,
219: Exception {
220: return query(req.getQueryType(), req);
221: }
222:
223: /**
224: * Processes a "query" using a user constructed SolrQueryRequest
225: *
226: * @param handler the name of the request handler to process the request
227: * @param req the Query to process, will be closed.
228: * @return The XML response to the query
229: * @exception Exception any exception in the response.
230: * @exception IOException if there is a problem writing the XML
231: * @see LocalSolrQueryRequest
232: */
233: public String query(String handler, SolrQueryRequest req)
234: throws IOException, Exception {
235: SolrQueryResponse rsp = new SolrQueryResponse();
236: core.execute(core.getRequestHandler(handler), req, rsp);
237: if (rsp.getException() != null) {
238: throw rsp.getException();
239: }
240:
241: StringWriter sw = new StringWriter(32000);
242: QueryResponseWriter responseWriter = core
243: .getQueryResponseWriter(req);
244: responseWriter.write(sw, req, rsp);
245:
246: req.close();
247:
248: return sw.toString();
249: }
250:
251: /**
252: * A helper method which valides a String against an array of XPath test
253: * strings.
254: *
255: * @param xml The xml String to validate
256: * @param tests Array of XPath strings to test (in boolean mode) on the xml
257: * @return null if all good, otherwise the first test that fails.
258: */
259: public String validateXPath(String xml, String... tests)
260: throws XPathExpressionException, SAXException {
261:
262: if (tests == null || tests.length == 0)
263: return null;
264:
265: Document document = null;
266: try {
267: document = builder.parse(new ByteArrayInputStream(xml
268: .getBytes("UTF-8")));
269: } catch (UnsupportedEncodingException e1) {
270: throw new RuntimeException("Totally weird UTF-8 exception",
271: e1);
272: } catch (IOException e2) {
273: throw new RuntimeException("Totally weird io exception", e2);
274: }
275:
276: for (String xp : tests) {
277: xp = xp.trim();
278: Boolean bool = (Boolean) xpath.evaluate(xp, document,
279: XPathConstants.BOOLEAN);
280:
281: if (!bool) {
282: return xp;
283: }
284: }
285: return null;
286:
287: }
288:
289: public SolrCore getCore() {
290: return core;
291: }
292:
293: /**
294: * Shuts down and frees any resources
295: */
296: public void close() {
297: core.close();
298: }
299:
300: /**
301: * A helper that adds an xml <doc> containing all of the
302: * fields and values specified (odds are fields, evens are values)
303: * to a StringBuffer.
304: */
305: public void appendSimpleDoc(StringBuffer buf,
306: String... fieldsAndValues) throws IOException {
307:
308: buf.append(makeSimpleDoc(fieldsAndValues));
309: }
310:
311: /**
312: * A helper that creates an xml <doc> containing all of the
313: * fields and values specified
314: *
315: * @param fieldsAndValues 0 and Even numbered args are fields names odds are field values.
316: */
317: public static StringBuffer makeSimpleDoc(String... fieldsAndValues) {
318:
319: try {
320: StringWriter w = new StringWriter();
321: w.append("<doc>");
322: for (int i = 0; i < fieldsAndValues.length; i += 2) {
323: XML.writeXML(w, "field", fieldsAndValues[i + 1],
324: "name", fieldsAndValues[i]);
325: }
326: w.append("</doc>");
327: return w.getBuffer();
328: } catch (IOException e) {
329: throw new RuntimeException(
330: "this should never happen with a StringWriter", e);
331: }
332: }
333:
334: /**
335: * Generates a delete by query xml string
336: * @param q Query that has not already been xml escaped
337: */
338: public static String deleteByQuery(String q) {
339: return delete("query", q);
340: }
341:
342: /**
343: * Generates a delete by id xml string
344: * @param id ID that has not already been xml escaped
345: */
346: public static String deleteById(String id) {
347: return delete("id", id);
348: }
349:
350: /**
351: * Generates a delete xml string
352: * @param val text that has not already been xml escaped
353: */
354: private static String delete(String deltype, String val) {
355: try {
356: StringWriter r = new StringWriter();
357:
358: r.write("<delete>");
359: XML.writeXML(r, deltype, val);
360: r.write("</delete>");
361:
362: return r.getBuffer().toString();
363: } catch (IOException e) {
364: throw new RuntimeException(
365: "this should never happen with a StringWriter", e);
366: }
367: }
368:
369: /**
370: * Helper that returns an <optimize> String with
371: * optional key/val pairs.
372: *
373: * @param args 0 and Even numbered args are params, Odd numbered args are values.
374: */
375: public static String optimize(String... args) {
376: return simpleTag("optimize", args);
377: }
378:
379: private static String simpleTag(String tag, String... args) {
380: try {
381: StringWriter r = new StringWriter();
382:
383: // this is anoying
384: if (null == args || 0 == args.length) {
385: XML.writeXML(r, tag, null);
386: } else {
387: XML.writeXML(r, tag, null, (Object) args);
388: }
389: return r.getBuffer().toString();
390: } catch (IOException e) {
391: throw new RuntimeException(
392: "this should never happen with a StringWriter", e);
393: }
394: }
395:
396: /**
397: * Helper that returns an <commit> String with
398: * optional key/val pairs.
399: *
400: * @param args 0 and Even numbered args are params, Odd numbered args are values.
401: */
402: public static String commit(String... args) {
403: return simpleTag("commit", args);
404: }
405:
406: public LocalRequestFactory getRequestFactory(String qtype,
407: int start, int limit) {
408: LocalRequestFactory f = new LocalRequestFactory();
409: f.qtype = qtype;
410: f.start = start;
411: f.limit = limit;
412: return f;
413: }
414:
415: /**
416: * 0 and Even numbered args are keys, Odd numbered args are values.
417: */
418: public LocalRequestFactory getRequestFactory(String qtype,
419: int start, int limit, String... args) {
420: LocalRequestFactory f = getRequestFactory(qtype, start, limit);
421: for (int i = 0; i < args.length; i += 2) {
422: f.args.put(args[i], args[i + 1]);
423: }
424: return f;
425:
426: }
427:
428: public LocalRequestFactory getRequestFactory(String qtype,
429: int start, int limit, Map<String, String> args) {
430:
431: LocalRequestFactory f = getRequestFactory(qtype, start, limit);
432: f.args.putAll(args);
433: return f;
434: }
435:
436: /**
437: * A Factory that generates LocalSolrQueryRequest objects using a
438: * specified set of default options.
439: */
440: public class LocalRequestFactory {
441: public String qtype = "standard";
442: public int start = 0;
443: public int limit = 1000;
444: public Map<String, String> args = new HashMap<String, String>();
445:
446: public LocalRequestFactory() {
447: }
448:
449: public LocalSolrQueryRequest makeRequest(String... q) {
450: if (q.length == 1) {
451: return new LocalSolrQueryRequest(TestHarness.this
452: .getCore(), q[0], qtype, start, limit, args);
453: }
454:
455: return new LocalSolrQueryRequest(
456: TestHarness.this .getCore(), new NamedList(Arrays
457: .asList(q)));
458: }
459: }
460: }
|