package org.cdlib.xtf.textEngine; import java.io.StringReader; import java.text.DecimalFormat; import java.util.Iterator; import java.util.Set; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import org.apache.lucene.search.Explanation; import org.cdlib.xtf.servletBase.TextServlet; import org.cdlib.xtf.textEngine.facet.ResultFacet; import org.cdlib.xtf.textEngine.facet.ResultGroup; import org.cdlib.xtf.util.Attrib; /** * Copyright (c) 2004, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * Represents the results of a query. This consists of a few statistics, * followed by an array of document hit(s). * * @author Martin Haye */ public class QueryResult { /** * Context of the query (including stop word list, and maps for * plurals and accents). CrossQuery doesn't use the context, but dynaXML * does. */ public QueryContext context; /** * A set that can be used to check whether a given term is present * in the original query that produced this hit. Only applies to the "text" * field (i.e. the full text of the document.) CrossQuery doesn't use * the text term set, but dynaXML does. */ public Set textTerms; /** * Total number of documents matched by the query (possibly many more * than are returned in this particular request.) */ public int totalDocs; /** Ordinal rank of the first document hit returned (0-based) */ public int startDoc; /** Oridinal rank of the last document hit returned, plus 1 */ public int endDoc; /** * Whether document scores were normalized so that highest ranking doc * has score 100. */ public boolean scoresNormalized; /** One hit per document */ public DocHit[] docHits; /** Faceted results grouped by field value (if specified in query) */ public ResultFacet[] facets; /** Spelling suggestions for query terms (if spellcheck specified) */ public SpellingSuggestion[] suggestions; /** Formatter for non-normalized scores */ private DecimalFormat decFormat; /** * Makes an XML document out of the list of document hits, and returns a * Source object that represents it. * * @param mainTagName Name of the top-level tag to generate (e.g. * "crossQueryResult", etc.) * @param extraStuff Additional XML to insert into the query * result document. Typically includes <parameters> * block and <query> block. * @return XML Source containing all the hits and snippets. */ public Source hitsToSource(String mainTagName, String extraStuff) { String str = hitsToString(mainTagName, extraStuff); return new StreamSource(new StringReader(str)); } // hitsToSource() /** * Makes an XML document out of the list of document hits, and returns a * String object that represents it. * * @param mainTagName Name of the top-level tag to generate (e.g. * "crossQueryResult", etc.) * @param extraStuff Additional XML to insert into the query * result document. Typically includes <parameters> * block and <query> block. * @return XML string containing all the hits and snippets. */ public String hitsToString(String mainTagName, String extraStuff) { StringBuffer buf = new StringBuffer(1000); buf.append( "<" + mainTagName + " totalDocs=\"" + totalDocs + "\" " + " startDoc=\"" + Math.min(startDoc + 1, endDoc) + "\" " + // Note above: 1-based start " endDoc=\"" + endDoc + "\">"); // If extra XML was specified, dump it in here. if (extraStuff != null) buf.append(extraStuff); // If spelling suggestions were made, put them in. if (suggestions != null) structureSuggestions(buf); // Add the top-level doc hits. structureDocHits(docHits, startDoc, buf); // If faceting was specified, add that info too. if (facets != null) { // Process each facet in turn for (int i = 0; i < facets.length; i++) { ResultFacet facet = facets[i]; buf.append( "<facet field=\"" + facet.field + "\" " + "totalGroups=\"" + facet.rootGroup.totalSubGroups + "\" " + "totalDocs=\"" + facet.rootGroup.totalDocs + "\">"); // Recursively process all the groups. if (facet.rootGroup.subGroups != null) { for (int j = 0; j < facet.rootGroup.subGroups.length; j++) structureGroup(facet.rootGroup.subGroups[j], buf); } buf.append("</facet>"); } // for i } // if // Add the final tag. buf.append("</" + mainTagName + ">\n"); // Now make the final string. return buf.toString(); } // hitsToString() /** * Does the work of turning faceted groups into XML. * * @param group The group to work on * @param buf Buffer to add XML to */ private void structureGroup(ResultGroup group, StringBuffer buf) { // Translate the "<empty>" marker to "" String groupValue = group.value; if (groupValue.equals("<empty>")) groupValue = ""; // Do the info for the group itself. buf.append( "<group value=\"" + TextServlet.makeHtmlString(groupValue) + "\" " + "rank=\"" + (group.rank + 1) + "\" " + "totalSubGroups=\"" + group.totalSubGroups + "\" " + "totalDocs=\"" + group.totalDocs + "\" " + "startDoc=\"" + (group.endDoc > 0 ? group.startDoc + 1 : 0) + "\" " + "endDoc=\"" + (group.endDoc) + "\">"); // If the group has any dochits, do them now. if (group.docHits != null) structureDocHits(group.docHits, group.startDoc, buf); // Do all the sub-groups. if (group.subGroups != null) { for (int i = 0; i < group.subGroups.length; i++) structureGroup(group.subGroups[i], buf); } // All done. buf.append("</group>"); } // structureGroup /** * Does the work of turning DocHits into XML. * * @param docHits Array of DocHits to structure * @param buf Buffer to add the XML to */ private void structureDocHits(DocHit[] docHits, int startDoc, StringBuffer buf) { if (docHits == null) return; for (int i = 0; i < docHits.length; i++) { DocHit docHit = docHits[i]; String scoreStr; if (scoresNormalized) scoreStr = Integer.toString(Math.round(docHit.score * 100)); else { if (decFormat == null) decFormat = (DecimalFormat)DecimalFormat.getInstance(); scoreStr = decFormat.format(docHit.score); } buf.append( "<docHit" + " rank=\"" + (i + startDoc + 1) + "\"" + " path=\"" + TextServlet.makeHtmlString(docHit.filePath()) + "\"" + " score=\"" + scoreStr + "\"" + " totalHits=\"" + docHit.totalSnippets() + "\""); if (docHit.recordNum() > 0) buf.append(" recordNum=\"" + docHit.recordNum() + "\""); if (docHit.subDocument() != null) buf.append(" subDocument=\"" + TextServlet.makeHtmlString(docHit.subDocument()) + "\""); buf.append(">\n"); Explanation explanation = docHit.explanation(); if (explanation != null) structureExplanation(explanation, buf); if (!docHit.metaData().isEmpty()) { buf.append("<meta>\n"); for (Iterator atts = docHit.metaData().iterator(); atts.hasNext();) { Attrib attrib = (Attrib)atts.next(); buf.append(attrib.value); } // for atts buf.append("</meta>\n"); } for (int j = 0; j < docHit.nSnippets(); j++) { Snippet snippet = docHit.snippet(j, true); buf.append( "<snippet rank=\"" + (j + 1) + "\" score=\"" + Math.round(snippet.score * 100) + "\""); if (snippet.sectionType != null) buf.append(" sectionType=\"" + snippet.sectionType + "\""); buf.append( ">" + TextServlet.makeHtmlString(snippet.text, true) + "</snippet>\n"); } // for j buf.append("</docHit>\n"); } // for i } // structureDocHits() /** * Does the work of turning a score explanation into XML. */ private void structureExplanation(Explanation exp, StringBuffer buf) { buf.append("<explanation value=\""); buf.append(exp.getValue()); buf.append("\" description=\""); buf.append(exp.getDescription()); buf.append("\">\n"); Explanation[] subs = exp.getDetails(); if (subs != null) { for (int i = 0; i < subs.length; i++) structureExplanation(subs[i], buf); } buf.append("</explanation>\n"); } // structureExplanation /** * Does the work of translating spelling suggestions into XML. */ private void structureSuggestions(StringBuffer buf) { buf.append("<spelling>\n"); for (int i = 0; i < suggestions.length; i++) { SpellingSuggestion sugg = suggestions[i]; StringBuffer fieldsBuf = new StringBuffer(); for (int j = 0; j < sugg.fields.length; j++) { if (fieldsBuf.length() > 0) fieldsBuf.append(","); fieldsBuf.append(sugg.fields[j]); } buf.append( " <suggestion" + " originalTerm=\"" + TextServlet.makeHtmlString(sugg.origTerm) + "\"" + " fields=\"" + fieldsBuf + "\"" + " suggestedTerm=\"" + (sugg.suggestedTerm != null ? TextServlet.makeHtmlString(sugg.suggestedTerm) : "") + "\"" + "/>\n"); } buf.append("</spelling>\n"); } // structureSuggestions() } // class QueryResult