SOLR-49 - new XSLTResponseWriter for server side XSLT processing, and a new QueryResponseWriter.init(NamedList) method

git-svn-id: https://svn.apache.org/repos/asf/incubator/solr/trunk@465317 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Chris M. Hostetter 2006-10-18 17:53:08 +00:00
parent a66dc55848
commit 1a12258057
14 changed files with 412 additions and 7 deletions

View File

@ -55,7 +55,11 @@ New Features
invariant params that can not overridden in the query. (hossman, SOLR-46)
26. Default operator for query parsing can now be specified with q.op=AND|OR
from the client request, overriding the schema value. (ehatcher)
27. New XSLTResponseWriter does server side XSLT processing of XML Response.
In the process, an init(NamedList) method was added to QueryResponseWriter
which works the same way as SolrRequestHandler.
(Bertrand Delacretaz / SOLR-49 / hossman)
Changes in runtime behavior
1. classes reorganized into different packages, package names changed to Apache
2. force read of document stored fields in QuerySenderListener

View File

@ -299,7 +299,16 @@
<queryResponseWriter name="custom" class="com.example.MyResponseWriter"/>
-->
<!--
XSLT response writer (SOLR-49)
Changes to XSLT transforms are taken into account every xsltCacheLifetimeSeconds at most.
-->
<queryResponseWriter
name="xslt"
class="org.apache.solr.request.XSLTResponseWriter"
>
<int name="xsltCacheLifetimeSeconds">5</int>
</queryResponseWriter>
<!-- config for the admin interface -->
<admin>

View File

@ -0,0 +1,78 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
* Copyright 2006 The Apache Software Foundation
*
* 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.
-->
<!--
Simple transform of Solr query results to HTML
-->
<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
>
<xsl:output media-type="text/html"/>
<xsl:variable name="title" select="concat('Solr search results (',response/result/@numFound,' documents)')"/>
<xsl:template match='/'>
<html>
<head>
<title><xsl:value-of select="$title"/></title>
<xsl:call-template name="css"/>
</head>
<body>
<h1><xsl:value-of select="$title"/></h1>
<div class="note">
This has been formatted by the default query-to-html.xsl transform - use your own XSLT
to get a nicer page
</div>
<xsl:apply-templates select="response/result/doc"/>
</body>
</html>
</xsl:template>
<xsl:template match="doc">
<div class="doc">
<table width="100%">
<xsl:apply-templates/>
</table>
</div>
</xsl:template>
<xsl:template match="doc/*">
<tr>
<td class="name">
<xsl:value-of select="@name"/>
</td>
<td class="value">
<xsl:value-of select="."/>
</td>
</tr>
</xsl:template>
<xsl:template match="*"/>
<xsl:template name="css">
<style type="text/css">
body { font-family: "Lucida Grande", sans-serif }
.doc { margin-top: 1em; border-top: solid grey 1px; }
td.name { font-style: italic; font-size:80%; }
td { vertical-align: top; }
.note { font-size:80%; }
</style>
</xsl:template>
</xsl:stylesheet>

View File

@ -966,6 +966,7 @@ public final class SolrCore {
log.info("adding queryResponseWriter "+name+"="+className);
QueryResponseWriter writer = (QueryResponseWriter) Config.newInstance(className);
writer.init(DOMUtil.childNodesToNamedList(elm));
responseWriters.put(name, writer);
} catch (Exception ex) {
SolrException.logOnce(log,null, ex);

View File

@ -20,6 +20,8 @@ import java.util.*;
public class JSONResponseWriter implements QueryResponseWriter {
static String CONTENT_TYPE_JSON_UTF8="text/x-json; charset=UTF-8";
public void init(NamedList n) {
}
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
JSONWriter w = new JSONWriter(writer, req, rsp);
@ -716,4 +718,4 @@ class RubyWriter extends JSONWriter {
}
writer.write('\'');
}
}
}

View File

@ -3,9 +3,15 @@ package org.apache.solr.request;
import java.io.Writer;
import java.io.IOException;
import org.apache.solr.util.NamedList;
public class PythonResponseWriter implements QueryResponseWriter {
static String CONTENT_TYPE_PYTHON_ASCII="text/x-python;charset=US-ASCII";
public void init(NamedList n) {
/* NOOP */
}
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
PythonWriter w = new PythonWriter(writer, req, rsp);
w.writeResponse();

View File

@ -19,6 +19,8 @@ package org.apache.solr.request;
import java.io.Writer;
import java.io.IOException;
import org.apache.solr.util.NamedList;
/**
* Implementations of <code>QueryResponseWriter</code> are used to format responses to query requests.
*
@ -70,5 +72,12 @@ public interface QueryResponseWriter {
* @return a Content-Type string, which may not be null.
*/
public String getContentType(SolrQueryRequest request, SolrQueryResponse response);
/** <code>init</code> will be called just once, immediately after creation.
* <p>The args are user-level initialization parameters that
* may be specified when declaring a response writer in
* solrconfig.xml
*/
public void init(NamedList args);
}

View File

@ -3,10 +3,16 @@ package org.apache.solr.request;
import java.io.Writer;
import java.io.IOException;
import org.apache.solr.util.NamedList;
public class RubyResponseWriter implements QueryResponseWriter {
static String CONTENT_TYPE_RUBY_UTF8="text/x-ruby;charset=UTF-8";
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
public void init(NamedList n) {
/* NOOP */
}
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
RubyWriter w = new RubyWriter(writer, req, rsp);
w.writeResponse();
}

View File

@ -19,12 +19,19 @@ package org.apache.solr.request;
import java.io.Writer;
import java.io.IOException;
import org.apache.solr.util.NamedList;
/**
* @author yonik
* @version $Id$
*/
public class XMLResponseWriter implements QueryResponseWriter {
public void init(NamedList n) {
/* NOOP */
}
public void write(Writer writer, SolrQueryRequest req, SolrQueryResponse rsp) throws IOException {
XMLWriter.writeResponse(writer,req,rsp);
}

View File

@ -0,0 +1,119 @@
/**
* Copyright 2006 The Apache Software Foundation
*
* 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 org.apache.solr.request;
import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.util.NamedList;
import org.apache.solr.util.xslt.TransformerProvider;
/** QueryResponseWriter which captures the output of the XMLWriter
* (in memory for now, not optimal performancewise), and applies an XSLT transform
* to it.
*/
public class XSLTResponseWriter implements QueryResponseWriter {
public static final String DEFAULT_CONTENT_TYPE = "text/xml";
public static final String TRANSFORM_PARAM = "tr";
public static final String CONTEXT_TRANSFORMER_KEY = "xsltwriter.transformer";
private Integer xsltCacheLifetimeSeconds = null;
public static final int XSLT_CACHE_DEFAULT = 60;
private static final String XSLT_CACHE_PARAM = "xsltCacheLifetimeSeconds";
private static final Logger log = Logger.getLogger(XSLTResponseWriter.class.getName());
public void init(NamedList n) {
final SolrParams p = SolrParams.toSolrParams(n);
xsltCacheLifetimeSeconds = p.getInt(XSLT_CACHE_PARAM,XSLT_CACHE_DEFAULT);
log.info("xsltCacheLifetimeSeconds=" + xsltCacheLifetimeSeconds);
}
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
Transformer t = null;
try {
t = getTransformer(request);
} catch(Exception e) {
// TODO should our parent interface throw (IO)Exception?
throw new RuntimeException("getTransformer fails in getContentType",e);
}
final String mediaTypeFromXslt = t.getOutputProperty("media-type");
if(mediaTypeFromXslt == null || mediaTypeFromXslt.length()==0) {
// This did not happen in my tests, mediaTypeFromXslt is set to "text/xml"
// if the XSLT transform does not contain an xsl:output element. Not sure
// if this is standard behavior or if it's just my JVM/libraries
return DEFAULT_CONTENT_TYPE;
}
return mediaTypeFromXslt;
}
public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
final Transformer t = getTransformer(request);
// capture the output of the XMLWriter
final CharArrayWriter w = new CharArrayWriter();
XMLWriter.writeResponse(w,request,response);
// and write transformed result to our writer
final Reader r = new BufferedReader(new CharArrayReader(w.toCharArray()));
final StreamSource source = new StreamSource(r);
final StreamResult result = new StreamResult(writer);
try {
t.transform(source, result);
} catch(TransformerException te) {
final IOException ioe = new IOException("XSLT transformation error");
ioe.initCause(te);
throw ioe;
}
}
/** Get Transformer from request context, or from TransformerProvider.
* This allows either getContentType(...) or write(...) to instantiate the Transformer,
* depending on which one is called first, then the other one reuses the same Transformer
*/
protected Transformer getTransformer(SolrQueryRequest request) throws IOException {
final String xslt = request.getParams().get(TRANSFORM_PARAM,null);
if(xslt==null) {
throw new IOException("'" + TRANSFORM_PARAM + "' request parameter is required to use the XSLTResponseWriter");
}
// no need to synchronize access to context, right?
// Nothing else happens with it at the same time
final Map<Object,Object> ctx = request.getContext();
Transformer result = (Transformer)ctx.get(CONTEXT_TRANSFORMER_KEY);
if(result==null) {
result = TransformerProvider.instance.getTransformer(xslt,xsltCacheLifetimeSeconds.intValue());
ctx.put(CONTEXT_TRANSFORMER_KEY,result);
}
return result;
}
}

View File

@ -0,0 +1,118 @@
/**
* Copyright 2006 The Apache Software Foundation
*
* 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 org.apache.solr.util.xslt;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.xml.transform.Templates;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;
import org.apache.solr.core.Config;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.request.XSLTResponseWriter;
/** Singleton that creates a Transformer for the XSLTServletFilter.
* For now, only caches the last created Transformer, but
* could evolve to use an LRU cache of Transformers.
*
* See http://www.javaworld.com/javaworld/jw-05-2003/jw-0502-xsl_p.html for
* one possible way of improving caching.
*/
public class TransformerProvider {
public static TransformerProvider instance = new TransformerProvider();
private final TransformerFactory tFactory = TransformerFactory.newInstance();
private String lastFilename;
private Templates lastTemplates = null;
private long cacheExpires = 0;
private static Logger log;
/** singleton */
private TransformerProvider() {
log = Logger.getLogger(TransformerProvider.class.getName());
// tell'em: currently, we only cache the last used XSLT transform, and blindly recompile it
// once cacheLifetimeSeconds expires
log.warning(
"The TransformerProvider's simplistic XSLT caching mechanism is not appropriate "
+ "for high load scenarios, unless a single XSLT transform is used"
+ " and xsltCacheLifetimeSeconds is set to a sufficiently high value."
);
}
/** Return a new Transformer, possibly created from our cached Templates object
* @throws TransformerConfigurationException
*/
public synchronized Transformer getTransformer(String filename,int cacheLifetimeSeconds) throws IOException {
// For now, the Templates are blindly reloaded once cacheExpires is over.
// It'd be better to check the file modification time to reload only if needed.
if(lastTemplates!=null && filename.equals(lastFilename) && System.currentTimeMillis() < cacheExpires) {
if(log.isLoggable(Level.FINE)) {
log.fine("Using cached Templates:" + filename);
}
} else {
lastTemplates = getTemplates(filename,cacheLifetimeSeconds);
}
Transformer result = null;
try {
result = lastTemplates.newTransformer();
} catch(TransformerConfigurationException tce) {
log.throwing(getClass().getName(), "getTransformer", tce);
final IOException ioe = new IOException("newTransformer fails ( " + lastFilename + ")");
ioe.initCause(tce);
throw ioe;
}
return result;
}
/** Return a Templates object for the given filename */
private Templates getTemplates(String filename,int cacheLifetimeSeconds) throws IOException {
Templates result = null;
lastFilename = null;
try {
if(log.isLoggable(Level.FINE)) {
log.fine("compiling XSLT templates:" + filename);
}
final InputStream xsltStream = Config.openResource("xslt/" + filename);
result = tFactory.newTemplates(new StreamSource(xsltStream));
} catch (Exception e) {
log.throwing(getClass().getName(), "newTemplates", e);
final IOException ioe = new IOException("Unable to initialize Templates '" + filename + "'");
ioe.initCause(e);
throw ioe;
}
lastFilename = filename;
lastTemplates = result;
cacheExpires = System.currentTimeMillis() + (cacheLifetimeSeconds * 1000);
return result;
}
}

View File

@ -22,6 +22,7 @@ import org.apache.solr.request.QueryResponseWriter;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrQueryResponse;
import org.apache.solr.util.AbstractSolrTestCase;
import org.apache.solr.util.NamedList;
import org.apache.solr.util.TestHarness;
/** Tests the ability to configure multiple query output writers, and select those
@ -34,7 +35,6 @@ public class OutputWriterTest extends AbstractSolrTestCase {
/** The XML string that's output for testing purposes. */
public static final String USELESS_OUTPUT = "useless output";
public String getSchemaFile() { return "solr/crazy-path-to-schema.xml"; }
public String getSolrConfigFile() { return "solr/crazy-path-to-config.xml"; }
@ -54,6 +54,14 @@ public class OutputWriterTest extends AbstractSolrTestCase {
assertEquals(USELESS_OUTPUT, out);
}
public void testTrivialXsltWriter() throws Exception {
lrf.args.put("wt", "xslt");
lrf.args.put("tr", "dummy.xsl");
String out = h.query(req("foo"));
System.out.println(out);
assertTrue(out.contains("DUMMY"));
}
////////////////////////////////////////////////////////////////////////////
/** An output writer that doesn't do anything useful. */
@ -62,11 +70,10 @@ public class OutputWriterTest extends AbstractSolrTestCase {
public UselessOutputWriter() {}
public void init(NamedList n) {}
public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response)
throws IOException {
writer.write(USELESS_OUTPUT);
}

View File

@ -0,0 +1,38 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
* Copyright 2006 The Apache Software Foundation
*
* 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.
-->
<!--
Simple Dummy transform to demonstrate XSLTResponseWriter
-->
<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
>
<xsl:output media-type="text/plain"/>
<xsl:variable name="dumb" select="concat('DUM','MY')"/>
<xsl:template match='/'>
<xsl:value-of select="$dumb"/>
</xsl:template>
</xsl:stylesheet>

View File

@ -46,6 +46,7 @@
<queryResponseWriter name="standard" class="org.apache.solr.request.XMLResponseWriter"/>
<queryResponseWriter name="useless" class="org.apache.solr.OutputWriterTest$UselessOutputWriter"/>
<queryResponseWriter name="xslt" class="org.apache.solr.request.XSLTResponseWriter"/>
<admin>