// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.prelude.templates;
import com.yahoo.concurrent.CopyOnWriteHashMap;
import com.yahoo.io.ByteWriter;
import com.yahoo.net.URI;
import com.yahoo.prelude.fastsearch.FastHit;
import com.yahoo.search.Result;
import com.yahoo.search.grouping.result.HitRenderer;
import com.yahoo.search.result.*;
import com.yahoo.text.Utf8String;
import com.yahoo.text.XMLWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;
/**
*
A template set which provides XML rendering of results and hits.
*
* This can be extended to create custom programmatic templates.
* Create a subclass which has static inner classes extending DefaultTemplate for the templates
* you wish to override and call the set method for those templates in the subclass template set
* constructor. Some of the default templates contained utility functions, and can be overridden
* in place of DefaultTemplate to gain access to these. See TiledTemplateSet for an example.
*
* @author bratseth
* @deprecated use JsonRenderer instead
*/
@SuppressWarnings("deprecation")
@Deprecated // TODO: Remove on Vespa 7
public class DefaultTemplateSet extends UserTemplate {
private static final Utf8String RESULT = new Utf8String("result");
private static final Utf8String GROUP = new Utf8String("group");
private static final Utf8String ID = new Utf8String("id");
private static final Utf8String FIELD = new Utf8String("field");
private static final Utf8String HIT = new Utf8String("hit");
private static final Utf8String ERROR = new Utf8String("error");
private static final Utf8String TOTAL_HIT_COUNT = new Utf8String("total-hit-count");
private static final Utf8String QUERY_TIME = new Utf8String("querytime");
private static final Utf8String SUMMARY_FETCH_TIME = new Utf8String("summaryfetchtime");
private static final Utf8String SEARCH_TIME = new Utf8String("searchtime");
private static final Utf8String NAME = new Utf8String("name");
private static final Utf8String CODE = new Utf8String("code");
private static final Utf8String COVERAGE_DOCS = new Utf8String("coverage-docs");
private static final Utf8String COVERAGE_NODES = new Utf8String("coverage-nodes");
private static final Utf8String COVERAGE_FULL = new Utf8String("coverage-full");
private static final Utf8String COVERAGE = new Utf8String("coverage");
private static final Utf8String RESULTS_FULL = new Utf8String("results-full");
private static final Utf8String RESULTS = new Utf8String("results");
private static final Utf8String TYPE = new Utf8String("type");
private static final Utf8String RELEVANCY = new Utf8String("relevancy");
private static final Utf8String SOURCE = new Utf8String("source");
private final CopyOnWriteHashMap fieldNameMap = new CopyOnWriteHashMap<>();
/**
* Create a template set with a name. This will be initialized with the default templates -
* use the set methods from the subclass constructor to override any of these with other template classes.
*/
protected DefaultTemplateSet(String name) {
super(name,
DEFAULT_MIMETYPE,
DEFAULT_ENCODING
);
}
public DefaultTemplateSet() {
this("default");
}
/** Uses an XML writer in this template */
@Override
public XMLWriter wrapWriter(Writer writer) {
return XMLWriter.from(writer, 10, -1);
}
@Override
public void header(Context context, XMLWriter writer) throws IOException {
Result result=(Result)context.get("result");
// TODO: move setting this to Result
context.setUtf8Output("utf-8".equalsIgnoreCase(getRequestedEncoding(result.getQuery())));
writer.xmlHeader(getRequestedEncoding(result.getQuery()));
writer.openTag(RESULT).attribute(TOTAL_HIT_COUNT,String.valueOf(result.getTotalHitCount()));
renderCoverageAttributes(result.getCoverage(false), writer);
renderTime(writer, result);
writer.closeStartTag();
}
private void renderTime(final XMLWriter writer, final Result result) {
if (!result.getQuery().getPresentation().getTiming()) {
return;
}
final String threeDecimals = "%.3f";
final double milli = .001d;
final long now = System.currentTimeMillis();
final long searchTime = now - result.getElapsedTime().first();
final double searchSeconds = ((double) searchTime) * milli;
if (result.getElapsedTime().firstFill() != 0L) {
final long queryTime = result.getElapsedTime().weightedSearchTime();
final long summaryFetchTime = result.getElapsedTime().weightedFillTime();
final double querySeconds = ((double) queryTime) * milli;
final double summarySeconds = ((double) summaryFetchTime) * milli;
writer.attribute(QUERY_TIME, String.format(threeDecimals, querySeconds));
writer.attribute(SUMMARY_FETCH_TIME, String.format(threeDecimals, summarySeconds));
}
writer.attribute(SEARCH_TIME, String.format(threeDecimals, searchSeconds));
}
@Override
public void footer(Context context, XMLWriter writer) throws IOException {
writer.closeTag();
}
@Override
/**
* Renders the header of a hit.
* Post-condition: The hit tag is open in this XML writer
*/
public void hit(Context context, XMLWriter writer) throws IOException {
Hit hit=(Hit)context.get("hit");
if (hit instanceof HitGroup) {
renderHitGroup((HitGroup) hit, context, writer);
} else {
writer.openTag(HIT);
renderHitAttributes(hit,writer);
writer.closeStartTag();
renderHitFields(context, hit, writer);
}
}
@Override
/**
* Renders the footer of a hit.
*
* Pre-condition: The hit tag is open in this XML writer.
* Post-condition: The hit tag is closed
*/
public void hitFooter(Context context, XMLWriter writer) throws IOException {
writer.closeTag();
}
@Override
public void error(Context context, XMLWriter writer) throws IOException {
ErrorMessage error=((Result)context.get("result")).hits().getError();
writer.openTag(ERROR).attribute(CODE,error.getCode()).content(error.getMessage(),false).closeTag();
}
@Override
public void noHits(Context context, XMLWriter writer) throws IOException {
// no hits, do nothing :)
}
protected static void renderCoverageAttributes(Coverage coverage, XMLWriter writer) throws IOException {
if (coverage == null) return;
writer.attribute(COVERAGE_DOCS,coverage.getDocs());
writer.attribute(COVERAGE_NODES,coverage.getNodes());
writer.attribute(COVERAGE_FULL,coverage.getFull());
writer.attribute(COVERAGE,coverage.getResultPercentage());
writer.attribute(RESULTS_FULL,coverage.getFullResultSets());
writer.attribute(RESULTS,coverage.getResultSets());
}
/**
* Writes a hit's default attributes like 'type', 'source', 'relevancy'.
*/
protected void renderHitAttributes(Hit hit,XMLWriter writer) throws IOException {
writer.attribute(TYPE,hit.getTypeString());
if (hit.getRelevance() != null) {
writer.attribute(RELEVANCY, hit.getRelevance().toString());
}
writer.attribute(SOURCE, hit.getSource());
}
/** Opens (but does not close) the group hit tag */
protected void renderHitGroup(HitGroup hit, Context context, XMLWriter writer) throws IOException {
if (HitRenderer.renderHeader(hit, writer)) {
// empty
} else if (hit.types().contains("grouphit")) {
// TODO Keep this?
renderHitGroupOfTypeGroupHit(context, hit, writer);
} else {
renderGroup(hit, writer);
}
}
/**
* Renders a hit group.
*/
protected void renderGroup(HitGroup hit, XMLWriter writer) throws IOException {
writer.openTag(GROUP);
renderHitAttributes(hit, writer);
writer.closeStartTag();
}
// Can't name this renderGroupHit as GroupHit is a class having nothing to do with HitGroup.
// Confused yet? Good!
protected void renderHitGroupOfTypeGroupHit(Context context, HitGroup hit, XMLWriter writer) throws IOException {
writer.openTag(HIT);
renderHitAttributes(hit, writer);
renderId(hit.getId(), writer);
writer.closeStartTag();
}
protected void renderId(URI uri, XMLWriter writer) throws IOException {
if (uri != null) {
writer.openTag(ID).content(uri.stringValue(),false).closeTag();
}
}
/**
* Renders all fields of a hit.
* Simply calls {@link #renderField(Context, Hit, java.util.Map.Entry, XMLWriter)} for every field.
*/
protected void renderHitFields(Context context, Hit hit, XMLWriter writer) throws IOException {
renderSyntheticRelevancyField(hit, writer);
for (Iterator> it = hit.fieldIterator(); it.hasNext(); ) {
renderField(context, hit, it.next(), writer);
}
}
private void renderSyntheticRelevancyField(Hit hit, XMLWriter writer) throws IOException {
final String relevancyFieldName = "relevancy";
final Relevance relevance = hit.getRelevance();
if (shouldRenderField(hit, relevancyFieldName) && relevance != null) {
renderSimpleField(relevancyFieldName, relevance, writer);
}
}
protected void renderField(Context context, Hit hit, Map.Entry entry, XMLWriter writer) throws IOException {
String fieldName = entry.getKey();
if (!shouldRenderField(hit, fieldName)) return;
if (fieldName.startsWith("$")) return; // Don't render fields that start with $ // TODO: Move to should render
writeOpenFieldElement(fieldName, writer);
renderFieldContent(context, hit, fieldName, writer);
writeCloseFieldElement(writer);
}
private void writeOpenFieldElement(String fieldName, XMLWriter writer) throws IOException {
Utf8String utf8 = fieldNameMap.get(fieldName);
if (utf8 == null) {
utf8 = new Utf8String(fieldName);
fieldNameMap.put(fieldName, utf8);
}
writer.openTag(FIELD).attribute(NAME, utf8);
writer.closeStartTag();
}
private void writeCloseFieldElement(XMLWriter writer) throws IOException { // TODO: Collapse
writer.closeTag();
}
protected void renderFieldContent(Context context, Hit hit,
String name, XMLWriter writer)
throws IOException {
boolean dumpedRaw = false;
if (hit instanceof FastHit && ((FastHit)hit).fieldIsNotDecoded(name)) {
writer.closeStartTag();
if ((writer.getWriter() instanceof ByteWriter) && context.isUtf8Output()) {
dumpedRaw = dumpBytes((ByteWriter) writer.getWriter(), (FastHit) hit, name);
}
if (dumpedRaw) {
writer.content("",false); // let the xml writer note that this tag had content
}
}
if (!dumpedRaw) {
String xmlval = hit.getFieldXML(name);
if (xmlval == null) {
xmlval = "(null)";
}
writer.escapedContent(xmlval,false);
}
}
private void renderSimpleField(String fieldName, Object fieldValue, XMLWriter writer) throws IOException {
writeOpenFieldElement(fieldName, writer);
writer.content(fieldValue.toString(),false);
writeCloseFieldElement(writer);
}
/** Returns whether a field should be rendered. This default implementation always returns true */
protected boolean shouldRenderField(Hit hit, String fieldName) {
// skip depending on hit type
return true;
}
}