mirror of https://github.com/apache/lucene.git
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:
parent
a66dc55848
commit
1a12258057
|
@ -55,6 +55,10 @@ 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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 RubyResponseWriter implements QueryResponseWriter {
|
||||
static String CONTENT_TYPE_RUBY_UTF8="text/x-ruby;charset=UTF-8";
|
||||
|
||||
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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue