com.marklogic.client.io.SearchHandle Maven / Gradle / Ivy
/*
* Copyright 2012-2016 MarkLogic Corporation
*
* Licensed 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 com.marklogic.client.io;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.marklogic.client.DatabaseClientFactory;
import com.marklogic.client.DatabaseClientFactory.HandleFactoryRegistry;
import com.marklogic.client.MarkLogicIOException;
import com.marklogic.client.impl.HandleAccessor;
import com.marklogic.client.impl.Utilities;
import com.marklogic.client.io.marker.ContentHandle;
import com.marklogic.client.io.marker.OperationNotSupported;
import com.marklogic.client.io.marker.SearchReadHandle;
import com.marklogic.client.io.marker.StructureReadHandle;
import com.marklogic.client.io.marker.XMLReadHandle;
import com.marklogic.client.query.ExtractedItem;
import com.marklogic.client.query.ExtractedResult;
import com.marklogic.client.query.FacetHeatmapValue;
import com.marklogic.client.query.FacetResult;
import com.marklogic.client.query.FacetValue;
import com.marklogic.client.query.MatchDocumentSummary;
import com.marklogic.client.query.MatchLocation;
import com.marklogic.client.query.MatchSnippet;
import com.marklogic.client.query.QueryDefinition;
import com.marklogic.client.query.SearchMetrics;
import com.marklogic.client.query.SearchResults;
/**
* A SearchHandle represents a set of search results returned by the server.
*
* The precise nature of the results returned depends on the query options used for the
* search and on the configuration of this handle.
*
* Snippets, in particular, are returned in various ways. In the default case, snippets are
* returned as Java objects. For custom or raw snippets, DOM documents are returned. The
* forceDOM
flag can be set to cause the handle to always return DOM documents,
* even in the default case.
*
*/
public class SearchHandle
extends BaseHandle
implements SearchReadHandle, SearchResults
{
static final private Logger logger = LoggerFactory.getLogger(SearchHandle.class);
static private String SEARCH_NS = "http://marklogic.com/appservices/search";
static private String QUERY_NS = "http://marklogic.com/cts/query";
private QueryDefinition querydef;
private HandleFactoryRegistry registry;
private MatchDocumentSummary[] summary;
private SearchMetrics metrics;
private ArrayList warnings;
private ArrayList reports;
private LinkedHashMap facets;
private LinkedHashMap constraints;
private EventRange planEvents;
private List events;
private long totalResults = -1;
private long start = -1;
private int pageLength = 0;
private String snippetType;
private String[] qtext;
private EventRange queryEvents;
public SearchHandle() {
super();
super.setFormat(Format.XML);
}
/**
* Sets the format associated with this handle.
*
* This handle only supports XML.
*
* @param format The format, which must be Format.XML or an exception will be raised.
*/
@Override
public void setFormat(Format format) {
if (format != Format.XML)
throw new IllegalArgumentException("SearchHandle supports the XML format only");
}
/**
* Fluent setter for the format associated with this handle.
*
* This handle only supports XML.
*
* @param format The format, which must be Format.XML or an exception will be raised.
* @return The SearchHandle instance on which this method was called.
*/
public SearchHandle withFormat(Format format) {
setFormat(format);
return this;
}
/**
* In a previous release, set the force DOM flag. This setter now has no effect
* and will be removed in a future release.
*
* @param forceDOM The flag setting.
*/
@Deprecated
public void setForceDOM(boolean forceDOM) {
}
@Override
protected Class receiveAs() {
return InputStream.class;
}
@Override
protected void receiveContent(InputStream content) {
try {
XMLInputFactory factory = XMLInputFactory.newFactory();
factory.setProperty("javax.xml.stream.isNamespaceAware", true);
factory.setProperty("javax.xml.stream.isValidating", false);
XMLEventReader reader = factory.createXMLEventReader(content, "UTF-8");
SearchResponseImpl response = new SearchResponseImpl();
response.parse(reader);
reader.close();
try {
content.close();
} catch (IOException e) {
// ignore.
}
summary =
(response.tempSummary == null || response.tempSummary.size() < 1) ?
new MatchDocumentSummary[0]:
response.tempSummary.toArray(new MatchDocumentSummary[response.tempSummary.size()]);
metrics = response.tempMetrics;
facets = response.tempFacets;
warnings = response.tempWarnings;
reports = response.tempReports;
planEvents = response.tempPlanEvents;
constraints = response.tempConstraints;
events = response.tempEvents;
totalResults = response.tempTotalResults;
start = response.tempStart;
pageLength = response.tempPageLength;
snippetType = response.tempSnippetType;
qtext =
(response.qtextList == null || response.qtextList.size() < 1) ?
null :
response.qtextList.toArray(new String[response.qtextList.size()]);
queryEvents = response.tempQueryEvents;
} catch (XMLStreamException e) {
throw new MarkLogicIOException("Could not construct search results: parser error", e);
}
}
/**
* Sets the query definition used in the search.
*
* Calling this method always deletes any cached search results.
*
* @param querydef The new QueryDefinition
*/
public void setQueryCriteria(QueryDefinition querydef) {
this.querydef = querydef;
summary = null;
metrics = null;
facets = null;
warnings = null;
reports = null;
planEvents = null;
constraints = null;
events = null;
totalResults = -1;
start = -1;
pageLength = 0;
snippetType = null;
qtext = null;
queryEvents = null;
}
/**
* Returns the query definition used for the search represented by this handle.
* @return The query definition.
*/
@Override
public QueryDefinition getQueryCriteria() {
return querydef;
}
/**
* Makes the handle registry for this database client available
* to this SearchHandle during processing of the search response.
* @param registry the registry of IO representation classes for this database client
*/
final public void setHandleRegistry(HandleFactoryRegistry registry) {
this.registry = registry;
}
private HandleFactoryRegistry getHandleRegistry() {
return this.registry;
}
/**
* Returns the total number of results in this search.
* @return The number of results.
*/
@Override
public long getTotalResults() {
return totalResults;
}
/**
* Returns the start page for this search.
* @return The offset to the first result.
*/
@Override
public long getStart() {
return start;
}
/**
* Returns the page length for this search.
* @return The number of results in a page.
*/
@Override
public int getPageLength() {
return pageLength;
}
/**
* Identifies whether results have default,
* raw document, customer, or no snippets.
* @return The type of snippets provided by results.
*/
@Override
public String getSnippetTransformType() {
return snippetType;
}
/**
* Returns the list of string queries, if specified
* by the query options.
* @return The string queries.
*/
@Override
public String[] getStringQueries() {
return qtext;
}
@Override
public T getQuery(T handle) {
return Utilities.exportToHandle(
getSlice(events, queryEvents), handle
);
}
/**
* Returns the metrics associated with this search.
* @return The metrics.
*/
@Override
public SearchMetrics getMetrics() {
return metrics;
}
/**
* Returns an array of summaries for the matched documents.
* @return The summary array.
*/
@Override
public MatchDocumentSummary[] getMatchResults() {
return summary;
}
/**
* Returns a list of the facet names returned in this search.
* @return The array of names.
*/
@Override
public String[] getFacetNames() {
if (facets == null || facets.isEmpty()) {
return new String[0];
}
Set names = facets.keySet();
return names.toArray(new String[names.size()]);
}
/**
* Returns the named facet results.
* @param name The name of the facet.
* @return Returns the results for the named facet or null if no such named facet exists.
*/
@Override
public FacetResult getFacetResult(String name) {
if (facets == null || facets.isEmpty()) {
return null;
}
return facets.get(name);
}
/**
* Returns an array of facet results for this search.
* @return The facets array.
*/
@Override
public FacetResult[] getFacetResults() {
if (facets == null || facets.isEmpty()) {
return new FacetResult[0];
}
Collection facetResults = facets.values();
return facetResults.toArray(new FacetResult[facetResults.size()]);
}
@Override
public String[] getConstraintNames() {
if (constraints == null || constraints.isEmpty()) {
return new String[0];
}
Set names = constraints.keySet();
return names.toArray(new String[names.size()]);
}
@Override
public T getConstraint(String name, T handle) {
if (constraints == null || constraints.isEmpty()) {
return null;
}
EventRange constraintEvents = constraints.get(name);
if (constraintEvents == null) {
return null;
}
return Utilities.exportToHandle(
getSlice(events, constraintEvents), handle
);
}
@Override
public Iterator getConstraintIterator(T handle) {
if (constraints == null || constraints.isEmpty()) {
List list = Collections.emptyList();
return list.iterator();
}
List constraintList =
new ArrayList(constraints.values());
return new EventIterator(events, constraintList, handle);
}
/**
* Returns the query plan associated with this search.
*
* Query plans are highly variable.
* @return the plan as a DOM Document
*/
@Override
public Document getPlan() {
DOMHandle handle = getPlan(new DOMHandle());
return (handle == null) ? null : handle.get();
}
@Override
public T getPlan(T handle) {
return Utilities.exportToHandle(
getSlice(events, planEvents), handle
);
}
/**
* Returns an array of any warnings associated with this search.
* @return The warnings array.
*/
@Override
public Warning[] getWarnings() {
return (warnings == null) ? new Warning[0] : warnings.toArray(new Warning[0]);
}
/**
* Returns an array of any reports associated with this search.
* @return The reports array.
*/
@Override
public Report[] getReports() {
return (reports == null) ? new Report[0] : reports.toArray(new Report[0]);
}
private List getSlice(
List eventList, EventRange eventRange
) {
if (eventList == null || eventRange == null) {
return null;
}
return eventList.subList(eventRange.getFirst(), eventRange.getNext());
}
private Document[] getEventDocuments(List eventList, List rangeList) {
if (rangeList == null || rangeList.size() < 1) {
return new Document[0];
}
ArrayList documents = new ArrayList();
DOMHandle handle = new DOMHandle();
for (int i=0; i < rangeList.size(); i++) {
EventRange eventRange = rangeList.get(i);
handle = Utilities.exportToHandle(
getSlice(eventList, eventRange), handle
);
Document document = (handle == null) ? null : handle.get();
if (document != null) {
documents.add(document);
}
}
int size = documents.size();
return (size == 0) ? null : documents.toArray(new Document[size]);
}
static private class SearchMetricsImpl implements SearchMetrics {
long qrTime = -1;
long frTime = -1;
long srTime = -1;
long mrTime = -1;
long totalTime = -1;
public SearchMetricsImpl(long qrTime, long frTime, long srTime, long mrTime, long totalTime) {
this.qrTime = qrTime;
this.frTime = frTime;
this.srTime = srTime;
this.mrTime = mrTime;
this.totalTime = totalTime;
}
@Override
public long getQueryResolutionTime() {
return qrTime;
}
@Override
public long getFacetResolutionTime() {
return frTime;
}
@Override
public long getSnippetResolutionTime() {
return srTime;
}
@Override
public long getMetadataResolutionTime() {
return mrTime;
}
@Override
public long getTotalTime() {
return totalTime;
}
}
static private class EventRange {
private int first = -1;
private int next = -1; // 1 after the last item in the range
private EventRange(int first, int next) {
super();
this.first = first;
this.next = next;
}
int getFirst() {
return first;
}
int getNext() {
return next;
}
}
private class MatchDocumentSummaryImpl implements MatchDocumentSummary {
private String uri = null;
private int score = -1;
private double conf = -1;
private double fit = -1;
private String path = null;
private ArrayList locations = new ArrayList();
private String mimeType = null;
private Format format = null;
private ArrayList snippetEvents;
private EventRange extractedEvents;
private EventRange metadataEvents;
private EventRange relevanceEvents;
private ArrayList similarUris;
private String extractSelected;
public MatchDocumentSummaryImpl(String uri, int score, double confidence, double fitness, String path, String mimeType, Format format, String extractSelected) {
this.uri = uri;
this.score = score;
conf = confidence;
fit = fitness;
this.path = path;
this.mimeType = mimeType;
this.format = format;
this.extractSelected = extractSelected;
}
@Override
public String getUri() {
return uri;
}
@Override
public int getScore() {
return score;
}
@Override
public double getConfidence() {
return conf;
}
@Override
public double getFitness() {
return fit;
}
@Override
public String getPath() {
return path;
}
@Override
public ExtractedResult getExtracted() {
ExtractedResultImpl result = new ExtractedResultImpl();
populateExtractedResult( result, events, extractedEvents );
return result;
}
private void populateExtractedResult(ExtractedResultImpl result, List events,
EventRange extractedEvents)
{
int start = extractedEvents.first;
int end = extractedEvents.next;
StartElement element = events.get(start).asStartElement();
QName elementName = element.getName();
if ( "extracted-none".equals(elementName.getLocalPart()) ) {
result.isEmpty = true;
} else {
@SuppressWarnings("unchecked")
Iterator attributes = element.getAttributes();
while ( attributes.hasNext() ) {
Attribute attr = attributes.next();
String attrName = attr.getName().getLocalPart();
if ( "kind".equals(attrName) ) {
result.kind = attr.getValue();
}
}
int startChildren = start + 1;
int endChildren = end - 1;
// now get the children (extracted items) as strings
EventRange extractedItemEvents = new EventRange(startChildren, endChildren);
if ( Format.XML == getFormat() ) {
result.setItems( populateExtractedItems(getSlice(events, extractedItemEvents)) );
// if extractSelected is "include", this is not a root document node
} else if ( Format.JSON == getFormat() && "include".equals(extractSelected) ) {
XMLEvent event = events.get(startChildren);
if ( XMLStreamConstants.CHARACTERS != event.getEventType() ) {
throw new MarkLogicIOException("Cannot parse JSON for " +
getPath() + "--content should be characters");
}
String json = event.asCharacters().getData();
try {
JsonNode jsonArray = new ObjectMapper().readTree(json);
ArrayList items = new ArrayList(jsonArray.size());
for ( JsonNode item : jsonArray ) {
items.add( item.toString() );
}
result.setItems( items );
} catch (Throwable e) {
throw new MarkLogicIOException("Cannot parse JSON '" + json + "' for " +
getPath(), e);
}
} else {
XMLEvent event = events.get(startChildren);
if ( XMLStreamConstants.CHARACTERS != event.getEventType() ) {
throw new MarkLogicIOException("Cannot read " +
getPath() + "--content should be characters");
}
String text = event.asCharacters().getData();
ArrayList items = new ArrayList(1);
items.add( event.asCharacters().getData() );
result.setItems( items );
}
}
}
private List populateExtractedItems(List events) {
List items = new ArrayList();
List itemEvents = new ArrayList();
List startNames = new ArrayList();
for ( XMLEvent event : events ) {
itemEvents.add(event);
switch (event.getEventType()) {
case XMLStreamConstants.START_ELEMENT: {
startNames.add(event.asStartElement().getName());
break;
}
case XMLStreamConstants.END_ELEMENT: {
QName startName = startNames.remove(startNames.size() - 1);
if (startNames.size() == 0 ) {
if ( startName.equals(event.asEndElement().getName())) {
items.add(Utilities.eventsToString(itemEvents));
itemEvents = new ArrayList();
} else {
throw new IllegalStateException("Error parsing xml \"" +
Utilities.eventsToString(itemEvents) + "\", element " + startName +
" doesn't end as expected");
}
}
break;
}
}
}
return items;
}
@Override
public T getFirstSnippetAs(Class as) {
ContentHandle handle = getHandleRegistry().makeHandle(as);
if (!XMLReadHandle.class.isAssignableFrom(handle.getClass())) {
throw new IllegalArgumentException("cannot read snippet from XML with "+handle.getClass());
}
if (null == getFirstSnippet((XMLReadHandle) handle)) {
return null;
}
return handle.get();
}
@Override
public T getFirstSnippet(T handle) {
if (snippetEvents == null || snippetEvents.size() < 1) {
return null;
}
return Utilities.exportToHandle(
getSlice(events, snippetEvents.get(0)), handle
);
}
@Override
public String getFirstSnippetText() {
if (snippetEvents == null || snippetEvents.size() < 1) {
return null;
}
return Utilities.eventTextToString(
getSlice(events, snippetEvents.get(0))
);
}
@Override
public Document[] getSnippets() {
return getEventDocuments(events, snippetEvents);
}
@Override
public Iterator getSnippetIterator(T handle) {
if (snippetEvents == null || snippetEvents.size() < 1) {
List list = Collections.emptyList();
return list.iterator();
}
return new EventIterator(events, snippetEvents, handle);
}
@Override
public MatchLocation[] getMatchLocations() {
if (locations == null) {
return new MatchLocation[0];
}
return locations.toArray(new MatchLocation[locations.size()]);
}
@Override
public Document getMetadata() {
DOMHandle handle = getMetadata(new DOMHandle());
return (handle == null) ? null : handle.get();
}
@Override
public T getMetadataAs(Class as) {
ContentHandle handle = getHandleRegistry().makeHandle(as);
if (!XMLReadHandle.class.isAssignableFrom(handle.getClass())) {
throw new IllegalArgumentException("cannot read metadata from XML with "+handle.getClass());
}
getMetadata((XMLReadHandle) handle);
return handle.get();
}
@Override
public T getMetadata(T handle) {
return Utilities.exportToHandle(
getSlice(events, metadataEvents), handle
);
}
@Override
public String getMimeType() {
return mimeType;
}
@Override
public Format getFormat() {
return format;
}
public void addLocation(MatchLocation loc) {
locations.add(loc);
}
@Override
public String[] getSimilarDocumentUris() {
if (similarUris == null || similarUris.size() < 1) {
return new String[0];
}
return similarUris.toArray(new String[similarUris.size()]);
}
@Override
public Document getRelevanceInfo() {
DOMHandle handle = getRelevanceInfo(new DOMHandle());
return (handle == null) ? null : handle.get();
}
@Override
public T getRelevanceInfo(T handle) {
return Utilities.exportToHandle(
getSlice(events, relevanceEvents), handle
);
}
}
static private class MatchLocationImpl implements MatchLocation {
private String path = null;
private ArrayList matchEvents = new ArrayList();
public MatchLocationImpl(String path) {
this.path = path;
}
@Override
public String getPath() {
return path;
}
@Override
public String getAllSnippetText() {
if (matchEvents == null) {
return null;
}
StringBuilder text = new StringBuilder();
for (MatchSnippet snippet : matchEvents) {
text.append(snippet.getText());
}
return text.toString();
}
@Override
public MatchSnippet[] getSnippets() {
if (matchEvents == null) {
return new MatchSnippet[0];
}
return matchEvents.toArray(new MatchSnippet[matchEvents.size()]);
}
public void addMatchSnippet(MatchSnippet s) {
matchEvents.add(s);
}
}
static private class MatchSnippetImpl implements MatchSnippet {
private boolean high = false;
private String text = null;
public MatchSnippetImpl(boolean high, String text) {
this.high = high;
this.text = text;
}
@Override
public boolean isHighlighted() {
return high;
}
@Override
public String getText() {
return text;
}
}
static private class FacetResultImpl implements FacetResult {
private String name = null;
private FacetValue[] values = null;
public FacetResultImpl(String name, FacetValue[] values) {
this.name = name;
this.values = values;
}
@Override
public String getName() {
return name;
}
@Override
public FacetValue[] getFacetValues() {
return values;
}
}
static private class FacetValueImpl implements FacetValue {
private String name = null;
private long count = 0;
private String label = null;
public FacetValueImpl(String name, long count) {
this.name = name;
this.count = count;
}
@Override
public String getName() {
return name;
}
@Override
public long getCount() {
return count;
}
@Override
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}
static private class FacetHeatmapValueImpl implements FacetHeatmapValue {
private String name = null;
private long count = 0;
private String label = null;
private double[] box = null;
public FacetHeatmapValueImpl(String name, long count, double s, double w, double n, double e) {
box = new double[4];
box[0] = s;
box[1] = w;
box[2] = n;
box[3] = e;
this.name = "[" + box[0] + ", " + box[1]+ ", " + box[2] + ", " + box[3] + "]";
this.count = count;
this.label = name;
}
@Override
public String getName() {
return name;
}
@Override
public long getCount() {
return count;
}
@Override
public String getLabel() {
return label;
}
@Override
public double[] getBox() {
return box;
}
}
/**
* Represents a warning.
*
* The Search API may return warnings, they are represented by objects in this class.
*/
static public class Warning {
private String id = null;
private String text = null;
protected Warning() {
}
/**
* Returns the ID of the warning.
* @return The id.
*/
public String getId() {
return id;
}
protected void setId(String id) {
this.id = id;
}
/**
* Returns the text of the warning message.
* @return The message.
*/
public String getMessage() {
return text;
}
protected void setMessage(String msg) {
text = msg;
}
}
/**
* Represents a report message.
*
* The Search API may return report messages, they are represented by objects in this class.
*/
static public class Report {
private String id = null;
private String name = null;
private String type = null;
private String text = null;
protected Report() {
}
/**
* Returns the ID of the message.
* @return The id.
*/
public String getId() {
return id;
}
protected void setId(String id) {
this.id = id;
}
/**
* Returns the name of the message.
* @return The name.
*/
public String getName() {
return name;
}
protected void setName(String name) {
this.name = name;
}
/**
* Returns the type of the message.
* @return The type.
*/
public String getType() {
return type;
}
protected void setType(String type) {
this.type = type;
}
/**
* Returns the text of the message.
* @return The message.
*/
public String getMessage() {
return text;
}
protected void setMessage(String msg) {
text = msg;
}
}
class EventIterator implements Iterator {
private List eventList;
private List rangeList;
private T handle;
private int nextEvent = 0;
EventIterator(List eventList, List rangeList, T handle) {
super();
this.eventList = eventList;
this.rangeList = rangeList;
this.handle = handle;
}
@Override
public boolean hasNext() {
return rangeList != null && nextEvent < rangeList.size();
}
@Override
public T next() {
if (!hasNext()) {
return null;
}
EventRange eventRange = rangeList.get(nextEvent++);
return Utilities.exportToHandle(
getSlice(eventList, eventRange), handle
);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Remove not supported");
}
}
private class SearchResponseImpl {
private ArrayList tempSummary;
private MatchDocumentSummaryImpl currSummary;
private ArrayList tempWarnings;
private ArrayList tempReports;
private SearchMetrics tempMetrics;
private EventRange tempPlanEvents;
private List tempEvents;
private long tempTotalResults = -1;
private long tempStart = -1;
private int tempPageLength = 0;
private LinkedHashMap tempFacets;
private LinkedHashMap tempConstraints;
private String tempSnippetType;
private String tempExtractSelected;
private ArrayList qtextList;
private EventRange tempQueryEvents;
private SearchResponseImpl() {
super();
}
private void parse(XMLEventReader reader) throws XMLStreamException {
tempEvents = new ArrayList();
while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
handleTop(reader, event.asStartElement());
break;
case XMLStreamConstants.END_ELEMENT:
case XMLStreamConstants.START_DOCUMENT:
case XMLStreamConstants.END_DOCUMENT:
case XMLStreamConstants.CDATA:
case XMLStreamConstants.CHARACTERS:
case XMLStreamConstants.ATTRIBUTE:
case XMLStreamConstants.COMMENT:
case XMLStreamConstants.DTD:
case XMLStreamConstants.ENTITY_DECLARATION:
case XMLStreamConstants.ENTITY_REFERENCE:
case XMLStreamConstants.NAMESPACE:
case XMLStreamConstants.NOTATION_DECLARATION:
case XMLStreamConstants.PROCESSING_INSTRUCTION:
case XMLStreamConstants.SPACE:
break;
default:
throw new InternalError("unknown event type: "+eventType);
}
}
}
private void handleTop(XMLEventReader reader, StartElement element) throws XMLStreamException {
QName name = element.getName();
if (!SEARCH_NS.equals(name.getNamespaceURI())) {
logger.warn("unexpected top element "+name.toString());
return;
}
String localName = name.getLocalPart();
if ("response".equals(localName)) { handleResponse(reader, element);
} else if ("result".equals(localName)) { handleResult(reader, element);
} else if ("facet".equals(localName)) { handleFacet(reader, element);
} else if ("boxes".equals(localName)) { handleGeoFacet(reader, element);
} else if ("qtext".equals(localName)) { handleQText(reader, element);
} else if ("query".equals(localName)) { handleQuery(reader, element);
} else if ("constraint".equals(localName)) { handleConstraint(reader, element);
} else if ("warning".equals(localName)) { handleWarning(reader, element);
} else if ("report".equals(localName)) { handleReport(reader, element);
} else if ("plan".equals(localName)) { handlePlan(reader, element);
} else if ("metrics".equals(localName)) { handleMetrics(reader, element);
} else {
logger.warn("Unexpected top search element "+name.toString());
}
}
private void handleResponse(XMLEventReader reader, StartElement element)
throws XMLStreamException {
tempSnippetType = getAttribute(element, "snippet-format");
if ( getAttribute(element, "total") != null ) {
tempTotalResults = Long.parseLong(getAttribute(element, "total"));
}
tempPageLength = Integer.parseInt(getAttribute(element, "page-length"));
tempStart = Long.parseLong(getAttribute(element, "start"));
tempExtractSelected = getAttribute(element, "selected");
collectTop(reader, element);
}
private void collectTop(XMLEventReader reader, StartElement element)
throws XMLStreamException {
QName startName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
handleTop(reader, event.asStartElement());
break;
case XMLStreamConstants.END_ELEMENT:
if (startName.equals(event.asEndElement().getName())) {
break events;
}
break;
}
}
}
private void handleResult(XMLEventReader reader, StartElement element)
throws XMLStreamException {
String ruri = getAttribute(element, "uri");
String path = getAttribute(element, "path");
String mimeType = getAttribute(element, "mimetype");
String formatString = getAttribute(element, "format");
Format format = Format.UNKNOWN;
if (formatString != null && !formatString.equals("")) {
format = Format.valueOf(formatString.toUpperCase());
}
int score = Integer.parseInt(getAttribute(element, "score"));
double confidence = Double.parseDouble(getAttribute(element, "confidence"));
double fitness = Double.parseDouble(getAttribute(element, "fitness"));
currSummary = new MatchDocumentSummaryImpl(
ruri, score, confidence, fitness, path, mimeType, format, tempExtractSelected);
if (tempSummary == null) {
tempSummary = new ArrayList();
}
tempSummary.add(currSummary);
collectResult(reader, element);
}
private void collectResult(XMLEventReader reader, StartElement element)
throws XMLStreamException {
QName snippetName = new QName(SEARCH_NS, "snippet");
QName extractedName = new QName(SEARCH_NS, "extracted");
QName extractedNoneName = new QName(SEARCH_NS, "extracted-none");
QName metadataName = new QName(SEARCH_NS, "metadata");
QName similarName = new QName(SEARCH_NS, "similar");
QName relevanceInfoName = new QName(QUERY_NS, "relevance-info");
ArrayList eventBuf = new ArrayList();
QName resultName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
eventType: switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
QName startName = startElement.getName();
if (snippetName.equals(startName)) {
handleSnippet(reader, startElement);
} else if (extractedName.equals(startName)) {
handleExtracted(reader, startElement);
} else if (extractedNoneName.equals(startName)) {
handleExtracted(reader, startElement);
} else if (metadataName.equals(startName)) {
handleMetadata(reader, startElement);
} else if (similarName.equals(startName)) {
handleSimilar(reader, startElement);
} else if (relevanceInfoName.equals(startName)) {
handleRelevanceInfo(reader, startElement);
} else {
break eventType;
}
// found result substructure, so cannot be a raw snippet
if (eventBuf != null) {
eventBuf = null;
}
break eventType;
case XMLStreamConstants.END_ELEMENT:
if (resultName.equals(event.asEndElement().getName())) {
break events;
}
break eventType;
}
// buffer candidates for a raw snippet
if (eventBuf != null) {
eventBuf.add(event);
}
}
// capture raw snippet
if (eventBuf != null) {
int first = tempEvents.size();
tempEvents.addAll(eventBuf);
addSnippet(new EventRange(first, tempEvents.size()));
}
}
private void handleExtracted(XMLEventReader reader, StartElement element)
throws XMLStreamException {
currSummary.extractedEvents = consumeEvents(reader, element);
}
private void handleMetadata(XMLEventReader reader, StartElement element)
throws XMLStreamException {
// TODO: populate map with element name/content key/value pairs
// TODO: special handling for constraint-meta, attribute-meta?
currSummary.metadataEvents = consumeEvents(reader, element);
}
private void handleSimilar(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (currSummary.similarUris == null) {
currSummary.similarUris = new ArrayList();
}
currSummary.similarUris.add(reader.getElementText());
}
private void handleRelevanceInfo(XMLEventReader reader, StartElement element)
throws XMLStreamException {
currSummary.relevanceEvents = consumeEvents(reader, element);
}
private void handlePlan(XMLEventReader reader, StartElement element)
throws XMLStreamException {
tempPlanEvents = consumeEvents(reader, element);
}
private void handleSnippet(XMLEventReader reader, StartElement element)
throws XMLStreamException {
int first = tempEvents.size();
tempEvents.add(element);
collectSnippet(reader, element);
addSnippet(new EventRange(first, tempEvents.size()));
}
private void collectSnippet(XMLEventReader reader, StartElement element)
throws XMLStreamException {
QName matchName = new QName(SEARCH_NS, "match");
QName snippetName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
tempEvents.add(event);
int eventType = event.getEventType();
eventType: switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
if (matchName.equals(startElement.getName())) {
handleMatch(reader, startElement);
break eventType;
}
break;
case XMLStreamConstants.END_ELEMENT:
if (snippetName.equals(event.asEndElement().getName())) {
break events;
}
break eventType;
}
}
}
private void addSnippet(EventRange snippetRange) {
if (currSummary.snippetEvents == null) {
currSummary.snippetEvents = new ArrayList();
}
currSummary.snippetEvents.add(snippetRange);
}
private void handleMatch(XMLEventReader reader, StartElement element)
throws XMLStreamException {
MatchLocationImpl location = new MatchLocationImpl(getAttribute(element, "path"));
QName highlightName = new QName(SEARCH_NS, "highlight");
StringBuilder buf = new StringBuilder();
// assumes that highlight elements do not nest
QName matchName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
tempEvents.add(event);
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
if (highlightName.equals(startElement.getName())) {
// add any text preceding a highlight
if (buf.length() > 0) {
location.addMatchSnippet(new MatchSnippetImpl(false, buf.toString()));
buf.setLength(0);
}
}
break;
case XMLStreamConstants.CDATA:
case XMLStreamConstants.CHARACTERS:
buf.append(event.asCharacters().getData());
break;
case XMLStreamConstants.END_ELEMENT:
EndElement endElement = event.asEndElement();
QName endName = endElement.getName();
if (matchName.equals(endName)) {
// add any text following the last highlight
if (buf.length() > 0) {
location.addMatchSnippet(new MatchSnippetImpl(false, buf.toString()));
}
break events;
} else if (highlightName.equals(endName)) {
// add any text contained by a highlight
location.addMatchSnippet(new MatchSnippetImpl(true, buf.toString()));
buf.setLength(0);
}
break;
}
}
buf = null;
currSummary.addLocation(location);
}
private void handleFacet(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (tempFacets == null) {
tempFacets = new LinkedHashMap();
}
String facetName = getAttribute(element, "name");
List values = new ArrayList();
QName facetValuesName = new QName(SEARCH_NS, "facet-value");
QName facetElementName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
if (facetValuesName.equals(startElement.getName())) {
values.add(handleFacetValue(reader, startElement));
} else {
logger.warn("Unexpected facet element "+startElement.getName().toString());
}
break;
case XMLStreamConstants.END_ELEMENT:
if (facetElementName.equals(event.asEndElement().getName())) {
break events;
}
break;
}
}
tempFacets.put(
facetName,
new FacetResultImpl(facetName,
values.toArray(new FacetValue[values.size()])));
}
private FacetValue handleFacetValue(XMLEventReader reader, StartElement element)
throws XMLStreamException {
String name = getAttribute(element, "name");
long count = Long.parseLong(getAttribute(element, "count"));
FacetValueImpl facetValue = new FacetValueImpl(name, count);
facetValue.setLabel(reader.getElementText());
return facetValue;
}
private void handleGeoFacet(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (tempFacets == null) {
tempFacets = new LinkedHashMap();
}
String facetName = getAttribute(element, "name");
List values = new ArrayList();
QName boxName = new QName(SEARCH_NS, "box");
QName boxesName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
if (boxName.equals(startElement.getName())) {
values.add(handleGeoFacetValue(reader, startElement));
} else {
logger.warn("Unexpected boxes element "+startElement.getName().toString());
}
break;
case XMLStreamConstants.END_ELEMENT:
if (boxesName.equals(event.asEndElement().getName())) {
break events;
}
break;
}
}
tempFacets.put(
facetName,
new FacetResultImpl(facetName,
values.toArray(new FacetValue[values.size()])));
}
private FacetValue handleGeoFacetValue(XMLEventReader reader, StartElement element) {
String name = getAttribute(element, "name");
long count = Long.parseLong(getAttribute(element, "count"));
double s = Double.parseDouble(getAttribute(element, "s"));
double w = Double.parseDouble(getAttribute(element, "w"));
double n = Double.parseDouble(getAttribute(element, "n"));
double e = Double.parseDouble(getAttribute(element, "e"));
return new FacetHeatmapValueImpl(name, count, s, w, n, e);
}
private void handleQText(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (qtextList == null) {
qtextList = new ArrayList();
}
qtextList.add(reader.getElementText());
}
private void handleQuery(XMLEventReader reader, StartElement element)
throws XMLStreamException {
tempQueryEvents = consumeEvents(reader, element);
}
private void handleMetrics(XMLEventReader reader, StartElement element)
throws XMLStreamException {
DatatypeFactory dtFactory;
try {
dtFactory = DatatypeFactory.newInstance();
} catch (DatatypeConfigurationException dce) {
throw new MarkLogicIOException("Cannot instantiate datatypeFactory", dce);
}
Calendar now = Calendar.getInstance();
QName queryName = new QName(SEARCH_NS, "query-resolution-time");
QName facetName = new QName(SEARCH_NS, "facet-resolution-time");
QName snippetName = new QName(SEARCH_NS, "snippet-resolution-time");
QName metadataName = new QName(SEARCH_NS, "metadata-resolution-time");
QName totalName = new QName(SEARCH_NS, "total-time");
long qrTime = -1;
long frTime = -1;
long srTime = -1;
long tTime = -1;
long mrTime = -1;
QName metricsName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.START_ELEMENT:
StartElement startElement = event.asStartElement();
QName startName = startElement.getName();
if (queryName.equals(startName)) {
qrTime = parseTime(dtFactory, now, reader.getElementText());
} else if (facetName.equals(startName)) {
frTime = parseTime(dtFactory, now, reader.getElementText());
} else if (snippetName.equals(startName)) {
srTime = parseTime(dtFactory, now, reader.getElementText());
} else if (metadataName.equals(startName)) {
mrTime = parseTime(dtFactory, now, reader.getElementText());
} else if (totalName.equals(startName)) {
tTime = parseTime(dtFactory, now, reader.getElementText());
} else {
logger.warn("Unexpected metrics element "+startName.toString());
}
break;
case XMLStreamConstants.END_ELEMENT:
if (metricsName.equals(event.asEndElement().getName())) {
break events;
}
break;
}
}
tempMetrics = new SearchMetricsImpl(qrTime, frTime, srTime, mrTime, tTime);
}
private void handleConstraint(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (tempConstraints == null) {
tempConstraints = new LinkedHashMap();
}
String constraintName = getAttribute(element, "name");
tempConstraints.put(constraintName, consumeEvents(reader, element));
}
private void handleWarning(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (tempWarnings == null) {
tempWarnings = new ArrayList();
}
Warning warning = new Warning();
warning.setId(getAttribute(element, "id"));
warning.setMessage(reader.getElementText());
tempWarnings.add(warning);
}
private void handleReport(XMLEventReader reader, StartElement element)
throws XMLStreamException {
if (tempReports == null) {
tempReports = new ArrayList();
}
Report report = new Report();
report.setId(getAttribute(element, "id"));
report.setName(getAttribute(element, "name"));
report.setType(getAttribute(element, "type"));
report.setMessage(reader.getElementText());
tempReports.add(report);
}
private String getAttribute(StartElement element, String name) {
Attribute att = element.getAttributeByName(new QName(name));
return (att != null) ? att.getValue() : null;
}
private long parseTime(DatatypeFactory dtFactory, Calendar now, String time) {
return dtFactory.newDurationDayTime(time).getTimeInMillis(now);
}
private EventRange consumeEvents(XMLEventReader reader, StartElement element)
throws XMLStreamException {
int first = tempEvents.size();
tempEvents.add(element);
QName startName = element.getName();
events: while (reader.hasNext()) {
XMLEvent event = reader.nextEvent();
tempEvents.add(event);
int eventType = event.getEventType();
switch (eventType) {
case XMLStreamConstants.END_ELEMENT:
if (startName.equals(event.asEndElement().getName())) {
break events;
}
}
}
return new EventRange(first, tempEvents.size());
}
}
static private class ExtractedItemImpl implements ExtractedItem {
String item;
public ExtractedItemImpl(String item) {
this.item = item;
}
public T get(T handle) {
HandleAccessor.receiveContent(handle, item);
return handle;
}
public T getAs(Class as) {
ContentHandle readHandle = DatabaseClientFactory.getHandleRegistry().makeHandle(as);
if ( readHandle == null ) return null;
HandleAccessor.receiveContent(readHandle, item);
return readHandle.get();
}
}
static private class ExtractedResultImpl implements ExtractedResult {
boolean isEmpty = false;
String kind;
private List itemStrings;
private List items;
private Iterator internalIterator;
public boolean isEmpty() {
return isEmpty;
}
public String getKind() {
return kind;
}
public int size() {
if ( items == null ) return 0;
return items.size();
}
public Iterator iterator() {
return items.iterator();
}
private void setItems(List itemStrings) {
if ( itemStrings == null ) return;
this.itemStrings = itemStrings;
items = new ArrayList(itemStrings.size());
for ( String itemString : itemStrings ) {
items.add( new ExtractedItemImpl(itemString) );
}
internalIterator = items.iterator();
}
public boolean hasNext() {
return internalIterator.hasNext();
}
public ExtractedItem next() {
return internalIterator.next();
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("ExtractedResult: ");
sb.append(isEmpty == true ? "isEmpty:[true] " : "");
sb.append(kind != null ? "kind:[" + kind + "] " : "");
for ( int i=1; i <= itemStrings.size(); i++ ) {
String item = itemStrings.get(i - 1);
sb.append("item_" + i + ":[" + item + "] ");
}
return sb.toString();
}
};
}