/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.response;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import org.apache.commons.lang.StringUtils;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.SolrResponseBase;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.SolrCore;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.apache.velocity.tools.ConversionUtils;
import org.apache.velocity.tools.generic.ComparisonDateTool;
import org.apache.velocity.tools.generic.DisplayTool;
import org.apache.velocity.tools.generic.EscapeTool;
import org.apache.velocity.tools.generic.ListTool;
import org.apache.velocity.tools.generic.LocaleConfig;
import org.apache.velocity.tools.generic.MathTool;
import org.apache.velocity.tools.generic.NumberTool;
import org.apache.velocity.tools.generic.ResourceTool;
import org.apache.velocity.tools.generic.SortTool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.solr.common.params.CommonParams.SORT;
public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAware {
// init param names, these are _only_ loaded at init time (no per-request control of these)
// - multiple different named writers could be created with different init params
public static final String TEMPLATE_BASE_DIR = "template.base.dir";
public static final String PARAMS_RESOURCE_LOADER_ENABLED = "params.resource.loader.enabled";
public static final String SOLR_RESOURCE_LOADER_ENABLED = "solr.resource.loader.enabled";
public static final String PROPERTIES_FILE = "init.properties.file";
// request param names
public static final String TEMPLATE = "v.template";
public static final String LAYOUT = "v.layout";
public static final String LAYOUT_ENABLED = "v.layout.enabled";
public static final String CONTENT_TYPE = "v.contentType";
public static final String JSON = "v.json";
public static final String LOCALE = "v.locale";
public static final String TEMPLATE_EXTENSION = ".vm";
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=UTF-8";
public static final String JSON_CONTENT_TYPE = "application/json;charset=UTF-8";
private File fileResourceLoaderBaseDir;
private boolean paramsResourceLoaderEnabled;
private boolean solrResourceLoaderEnabled;
private String initPropertiesFileName; // used just to hold from init() to inform()
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final SolrVelocityLogger velocityLogger = new SolrVelocityLogger(log);
private Properties velocityInitProps = new Properties();
private Map<String,String> customTools = new HashMap<String,String>();
@Override
public void init(NamedList args) {
fileResourceLoaderBaseDir = null;
String templateBaseDir = (String) args.get(TEMPLATE_BASE_DIR);
if (templateBaseDir != null && !templateBaseDir.isEmpty()) {
fileResourceLoaderBaseDir = new File(templateBaseDir).getAbsoluteFile();
if (!fileResourceLoaderBaseDir.exists()) { // "*not* exists" condition!
log.warn(TEMPLATE_BASE_DIR + " specified does not exist: " + fileResourceLoaderBaseDir);
fileResourceLoaderBaseDir = null;
} else {
if (!fileResourceLoaderBaseDir.isDirectory()) { // "*not* a directory" condition
log.warn(TEMPLATE_BASE_DIR + " specified is not a directory: " + fileResourceLoaderBaseDir);
fileResourceLoaderBaseDir = null;
}
}
}
// params resource loader: off by default
Boolean prle = args.getBooleanArg(PARAMS_RESOURCE_LOADER_ENABLED);
paramsResourceLoaderEnabled = (null == prle ? false : prle);
// solr resource loader: on by default
Boolean srle = args.getBooleanArg(SOLR_RESOURCE_LOADER_ENABLED);
solrResourceLoaderEnabled = (null == srle ? true : srle);
initPropertiesFileName = (String) args.get(PROPERTIES_FILE);
NamedList tools = (NamedList)args.get("tools");
if (tools != null) {
for(Object t : tools) {
Map.Entry tool = (Map.Entry)t;
customTools.put(tool.getKey().toString(), tool.getValue().toString());
}
}
}
@Override
public void inform(SolrCore core) {
// need to leverage SolrResourceLoader, so load init.properties.file here instead of init()
if (initPropertiesFileName != null) {
InputStream is = null;
try {
velocityInitProps.load(new InputStreamReader(core.getResourceLoader().openResource(initPropertiesFileName), StandardCharsets.UTF_8));
} catch (IOException e) {
log.warn("Error loading " + PROPERTIES_FILE + " specified property file: " + initPropertiesFileName, e);
}
}
}
@Override
public String getContentType(SolrQueryRequest request, SolrQueryResponse response) {
String contentType = request.getParams().get(CONTENT_TYPE);
// Use the v.contentType specified, or either of the default content types depending on the presence of v.json
return (contentType != null) ? contentType : ((request.getParams().get(JSON) == null) ? DEFAULT_CONTENT_TYPE : JSON_CONTENT_TYPE);
}
@Override
public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
VelocityEngine engine = createEngine(request); // TODO: have HTTP headers available for configuring engine
Template template = getTemplate(engine, request);
VelocityContext context = createContext(request, response);
context.put("engine", engine); // for $engine.resourceExists(...)
String layoutTemplate = request.getParams().get(LAYOUT);
boolean layoutEnabled = request.getParams().getBool(LAYOUT_ENABLED, true) && layoutTemplate != null;
String jsonWrapper = request.getParams().get(JSON);
boolean wrapResponse = layoutEnabled || jsonWrapper != null;
// create output
if (!wrapResponse) {
// straight-forward template/context merge to output
template.merge(context, writer);
}
else {
// merge to a string buffer, then wrap with layout and finally as JSON
StringWriter stringWriter = new StringWriter();
template.merge(context, stringWriter);
if (layoutEnabled) {
context.put("content", stringWriter.toString());
stringWriter = new StringWriter();
try {
engine.getTemplate(layoutTemplate + TEMPLATE_EXTENSION).merge(context, stringWriter);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
if (jsonWrapper != null) {
writer.write(jsonWrapper + "(");
writer.write(getJSONWrap(stringWriter.toString()));
writer.write(')');
} else { // using a layout, but not JSON wrapping
writer.write(stringWriter.toString());
}
}
}
private VelocityContext createContext(SolrQueryRequest request, SolrQueryResponse response) {
VelocityContext context = new VelocityContext();
// Register useful Velocity "tools"
String locale = request.getParams().get(LOCALE);
Map toolConfig = new HashMap();
toolConfig.put("locale", locale);
context.put("log", log); // TODO: add test; TODO: should this be overridable with a custom "log" named tool?
context.put("esc", new EscapeTool());
context.put("date", new ComparisonDateTool());
context.put("list", new ListTool());
context.put(SORT, new SortTool());
MathTool mathTool = new MathTool();
mathTool.configure(toolConfig);
context.put("math", mathTool);
NumberTool numberTool = new NumberTool();
numberTool.configure(toolConfig);
context.put("number", numberTool);
DisplayTool displayTool = new DisplayTool();
displayTool.configure(toolConfig);
context.put("display", displayTool);
ResourceTool resourceTool = new SolrVelocityResourceTool(request.getCore().getSolrConfig().getResourceLoader().getClassLoader());
resourceTool.configure(toolConfig);
context.put("resource", resourceTool);
/*
// Custom tools, specified in config as:
<queryResponseWriter name="velocityWithCustomTools" class="solr.VelocityResponseWriter">
<lst name="tools">
<str name="mytool">com.example.solr.velocity.MyTool</str>
</lst>
</queryResponseWriter>
*/
// Custom tools can override any of the built-in tools provided above, by registering one with the same name
for(String name : customTools.keySet()) {
Object customTool = SolrCore.createInstance(customTools.get(name), Object.class, "VrW custom tool: " + name, request.getCore(), request.getCore().getResourceLoader());
if (customTool instanceof LocaleConfig) {
((LocaleConfig)customTool).configure(toolConfig);
}
context.put(name, customTool);
}
// custom tools _cannot_ override context objects added below, like $request and $response
// TODO: at least log a warning when one of the *fixed* tools classes in name with a custom one, currently silently ignored
// Turn the SolrQueryResponse into a SolrResponse.
// QueryResponse has lots of conveniences suitable for a view
// Problem is, which SolrResponse class to use?
// One patch to SOLR-620 solved this by passing in a class name as
// as a parameter and using reflection and Solr's class loader to
// create a new instance. But for now the implementation simply
// uses QueryResponse, and if it chokes in a known way, fall back
// to bare bones SolrResponseBase.
// Can this writer know what the handler class is? With echoHandler=true it can get its string name at least
SolrResponse rsp = new QueryResponse();
NamedList<Object> parsedResponse = BinaryResponseWriter.getParsedResponse(request, response);
try {
rsp.setResponse(parsedResponse);
// page only injected if QueryResponse works
context.put("page", new PageTool(request, response)); // page tool only makes sense for a SearchHandler request
context.put("debug",((QueryResponse)rsp).getDebugMap());
} catch (ClassCastException e) {
// known edge case where QueryResponse's extraction assumes "response" is a SolrDocumentList
// (AnalysisRequestHandler emits a "response")
rsp = new SolrResponseBase();
rsp.setResponse(parsedResponse);
}
context.put("request", request);
context.put("response", rsp);
return context;
}
private VelocityEngine createEngine(SolrQueryRequest request) {
VelocityEngine engine = new VelocityEngine();
// route all Velocity logging through Solr's logging facility
engine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, velocityLogger);
// Set some engine properties that improve the experience
// - these could be considered in the future for parameterization, but can also be overridden by using
// the init.properties.file setting. (TODO: add a test for this properties set here overridden)
// load the built-in _macros.vm first, then load VM_global_library.vm for legacy (pre-5.0) support,
// and finally allow macros.vm to have the final say and override anything defined in the preceding files.
engine.setProperty(RuntimeConstants.VM_LIBRARY, "_macros.vm,VM_global_library.vm,macros.vm");
// Standard templates autoload, but not the macro one(s), by default, so let's just make life
// easier, and consistent, for macro development too.
engine.setProperty(RuntimeConstants.VM_LIBRARY_AUTORELOAD, "true");
/*
Set up Velocity resource loader(s)
terminology note: "resource loader" is overloaded here, there is Solr's resource loader facility for plugins,
and there are Velocity template resource loaders. It's confusing, they overlap: there is a Velocity resource
loader that loads templates from Solr's resource loader (SolrVelocityResourceLoader).
The Velocity resource loader order is [params,][file,][solr], intentionally ordered in this manner, and each
one optional and individually enable-able. By default, only "solr" (resource loader) is used, parsing templates
from a velocity/ sub-tree in either the classpath or under conf/.
A common usage would be to enable the file template loader, keeping the solr loader enabled; the Velocity resource
loader path would then be "file,solr" (params is disabled by default). The basic browse templates are built into
this plugin, but can be individually overridden by placing a same-named template in the template.base.dir specified
directory.
*/
ArrayList<String> loaders = new ArrayList<String>();
if (paramsResourceLoaderEnabled) {
loaders.add("params");
engine.setProperty("params.resource.loader.instance", new SolrParamResourceLoader(request));
}
if (fileResourceLoaderBaseDir != null) {
loaders.add("file");
engine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, fileResourceLoaderBaseDir.getAbsolutePath());
}
if (solrResourceLoaderEnabled) {
// The solr resource loader serves templates under a velocity/ subtree from <lib>, conf/,
// or SolrCloud's configuration tree. Or rather the other way around, other resource loaders are rooted
// from the top, whereas this is velocity/ sub-tree rooted.
loaders.add("solr");
engine.setProperty("solr.resource.loader.instance", new SolrVelocityResourceLoader(request.getCore().getSolrConfig().getResourceLoader()));
}
// Always have the built-in classpath loader. This is needed when using VM_LIBRARY macros, as they are required
// to be present if specified, and we want to have a nice macros facility built-in for users to use easily, and to
// extend in custom ways.
loaders.add("builtin");
engine.setProperty("builtin.resource.loader.instance", new ClasspathResourceLoader());
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, StringUtils.join(loaders,','));
engine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8");
// bring in any custom properties too
engine.init(velocityInitProps);
return engine;
}
private Template getTemplate(VelocityEngine engine, SolrQueryRequest request) throws IOException {
Template template;
String templateName = request.getParams().get(TEMPLATE);
String qt = request.getParams().get(CommonParams.QT);
String path = (String) request.getContext().get("path");
if (templateName == null && path != null) {
templateName = path;
} // TODO: path is never null, so qt won't get picked up maybe special case for '/select' to use qt, otherwise use path?
if (templateName == null && qt != null) {
templateName = qt;
}
if (templateName == null) templateName = "index";
try {
template = engine.getTemplate(templateName + TEMPLATE_EXTENSION);
} catch (Exception e) {
throw new IOException(e.getMessage());
}
return template;
}
private String getJSONWrap(String xmlResult) { // maybe noggit or Solr's JSON utilities can make this cleaner?
// escape the double quotes and backslashes
String replace1 = xmlResult.replaceAll("\\\\", "\\\\\\\\");
replace1 = replace1.replaceAll("\\n", "\\\\n");
replace1 = replace1.replaceAll("\\r", "\\\\r");
String replaced = replace1.replaceAll("\"", "\\\\\"");
// wrap it in a JSON object
return "{\"result\":\"" + replaced + "\"}";
}
// see: http://svn.apache.org/repos/asf/velocity/tools/branches/2.0.x/src/main/java/org/apache/velocity/tools/generic/ResourceTool.java
private static class SolrVelocityResourceTool extends ResourceTool {
private ClassLoader solrClassLoader;
public SolrVelocityResourceTool(ClassLoader cl) {
this.solrClassLoader = cl;
}
@Override
protected ResourceBundle getBundle(String baseName, Object loc) {
// resource bundles for this tool must be in velocity "package"
return ResourceBundle.getBundle(
"velocity." + baseName,
(loc == null) ? this.getLocale() : this.toLocale(loc),
solrClassLoader);
}
// Why did Velocity Tools make this private? Copied from ResourceTools.java
private Locale toLocale(Object obj) {
if (obj == null) {
return null;
}
if (obj instanceof Locale) {
return (Locale) obj;
}
String s = String.valueOf(obj);
return ConversionUtils.toLocale(s);
}
}
}