blob: 11aa6d08a6d90086c9896d863d4f871fee8b3941 [file] [log] [blame]
/*
* 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.sling.scripting.jsp;
import static org.apache.sling.api.scripting.SlingBindings.SLING;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingIOException;
import org.apache.sling.api.SlingServletException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.observation.ExternalResourceChangeListener;
import org.apache.sling.api.resource.observation.ResourceChange;
import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
import org.apache.sling.api.resource.observation.ResourceChangeListener;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.scripting.SlingScript;
import org.apache.sling.api.scripting.SlingScriptConstants;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.commons.classloader.ClassLoaderWriter;
import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
import org.apache.sling.commons.compiler.JavaCompiler;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.scripting.api.AbstractScriptEngineFactory;
import org.apache.sling.scripting.api.AbstractSlingScriptEngine;
import org.apache.sling.scripting.jsp.jasper.compiler.JspRuntimeContext;
import org.apache.sling.scripting.jsp.jasper.compiler.JspRuntimeContext.JspFactoryHandler;
import org.apache.sling.scripting.jsp.jasper.runtime.AnnotationProcessor;
import org.apache.sling.scripting.jsp.jasper.runtime.JspApplicationContextImpl;
import org.apache.sling.scripting.jsp.jasper.servlet.JspServletWrapper;
import org.apache.sling.scripting.jsp.util.TagUtil;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The JSP engine (a.k.a Jasper).
*
*/
@Component(label="%jsphandler.name",
description="%jsphandler.description",
metatype=true)
@Service(value={javax.script.ScriptEngineFactory.class,ResourceChangeListener.class,Servlet.class})
@Properties({
@Property(name="service.description",value="JSP Script Handler"),
@Property(name="service.vendor",value="The Apache Software Foundation"),
@Property(name="jasper.compilerTargetVM", value=JspServletOptions.AUTOMATIC_VERSION),
@Property(name="jasper.compilerSourceVM", value=JspServletOptions.AUTOMATIC_VERSION),
@Property(name="jasper.classdebuginfo",boolValue=true),
@Property(name="jasper.enablePooling",boolValue=true),
@Property(name="jasper.ieClassId",value="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"),
@Property(name="jasper.genStringAsCharArray",boolValue=false),
@Property(name="jasper.keepgenerated",boolValue=true),
@Property(name="jasper.mappedfile",boolValue=true),
@Property(name="jasper.trimSpaces",boolValue=false),
@Property(name="jasper.displaySourceFragments",boolValue=false),
@Property(name=ResourceChangeListener.PATHS, value={"/"}, propertyPrivate=true),
@Property(name="felix.webconsole.label", value="slingjsp", propertyPrivate=true),
@Property(name="felix.webconsole.title", value="JSP", propertyPrivate=true),
@Property(name="felix.webconsole.category", value="Sling", propertyPrivate=true)
})
public class JspScriptEngineFactory
extends AbstractScriptEngineFactory
implements Servlet,ResourceChangeListener,ExternalResourceChangeListener {
@Property(boolValue = true)
private static final String PROP_DEFAULT_IS_SESSION = "default.is.session";
/** Default logger */
private final Logger logger = LoggerFactory.getLogger(JspScriptEngineFactory.class);
@Reference(unbind="unbindSlingServletContext", target="(name=org.apache.sling)")
private ServletContext slingServletContext;
@Reference
private ClassLoaderWriter classLoaderWriter;
@Reference
private DynamicClassLoaderManager dynamicClassLoaderManager;
private ClassLoader dynamicClassLoader;
@Reference
private JavaCompiler javaCompiler;
/** The io provider for reading and writing. */
private SlingIOProvider ioProvider;
private SlingTldLocationsCache tldLocationsCache;
private JspRuntimeContext jspRuntimeContext;
private JspServletOptions options;
private JspServletContext jspServletContext;
private JspServletConfig servletConfig;
private boolean defaultIsSession;
/** The handler for the jsp factories. */
private JspFactoryHandler jspFactoryHandler;
public static final String[] SCRIPT_TYPE = { "jsp", "jspf", "jspx" };
public static final String[] NAMES = { "jsp", "JSP" };
public JspScriptEngineFactory() {
setExtensions(SCRIPT_TYPE);
setNames(NAMES);
}
/**
* @see javax.script.ScriptEngineFactory#getScriptEngine()
*/
@Override
public ScriptEngine getScriptEngine() {
return new JspScriptEngine();
}
/**
* @see javax.script.ScriptEngineFactory#getLanguageName()
*/
@Override
public String getLanguageName() {
return "Java Server Pages";
}
/**
* @see javax.script.ScriptEngineFactory#getLanguageVersion()
*/
@Override
public String getLanguageVersion() {
return "2.1";
}
/**
* @see javax.script.ScriptEngineFactory#getParameter(String)
*/
@Override
public Object getParameter(final String name) {
if ("THREADING".equals(name)) {
return "STATELESS";
}
return super.getParameter(name);
}
/**
* Call the error page
* @param bindings The bindings
* @param scriptHelper Script helper service
* @param context The script context
* @param scriptName The name of the script
*/
private void callErrorPageJsp(final Bindings bindings,
final SlingScriptHelper scriptHelper,
final ScriptContext context,
final String scriptName) {
final SlingBindings slingBindings = new SlingBindings();
slingBindings.putAll(bindings);
ResourceResolver resolver = (ResourceResolver) context.getAttribute(SlingScriptConstants.ATTR_SCRIPT_RESOURCE_RESOLVER,
SlingScriptConstants.SLING_SCOPE);
if ( resolver == null ) {
resolver = scriptHelper.getScript().getScriptResource().getResourceResolver();
}
final SlingIOProvider io = this.ioProvider;
final JspFactoryHandler jspfh = this.jspFactoryHandler;
// abort if JSP Support is shut down concurrently (SLING-2704)
if (io == null || jspfh == null) {
logger.warn("callJsp: JSP Script Engine seems to be shut down concurrently; not calling {}",
scriptHelper.getScript().getScriptResource().getPath());
return;
}
final ResourceResolver oldResolver = io.setRequestResourceResolver(resolver);
jspfh.incUsage();
try {
final JspServletWrapper errorJsp = getJspWrapper(scriptName, slingBindings);
errorJsp.service(slingBindings);
// The error page could be inside an include.
final SlingHttpServletRequest request = slingBindings.getRequest();
final Throwable t = (Throwable)request.getAttribute("javax.servlet.jsp.jspException");
final Object newException = request
.getAttribute("javax.servlet.error.exception");
// t==null means the attribute was not set.
if ((newException != null) && (newException == t)) {
request.removeAttribute("javax.servlet.error.exception");
}
// now clear the error code - to prevent double handling.
request.removeAttribute("javax.servlet.error.status_code");
request.removeAttribute("javax.servlet.error.request_uri");
request.removeAttribute("javax.servlet.error.status_code");
request.removeAttribute("javax.servlet.jsp.jspException");
} finally {
jspfh.decUsage();
io.resetRequestResourceResolver(oldResolver);
}
}
/**
* Call a JSP script
* @param bindings The bindings
* @param scriptHelper Script helper service
* @param context The script context
* @throws SlingServletException
* @throws SlingIOException
*/
private void callJsp(final Bindings bindings,
final SlingScriptHelper scriptHelper,
final ScriptContext context) {
ResourceResolver resolver = (ResourceResolver) context.getAttribute(SlingScriptConstants.ATTR_SCRIPT_RESOURCE_RESOLVER,
SlingScriptConstants.SLING_SCOPE);
if ( resolver == null ) {
resolver = scriptHelper.getScript().getScriptResource().getResourceResolver();
}
final SlingIOProvider io = this.ioProvider;
final JspFactoryHandler jspfh = this.jspFactoryHandler;
// abort if JSP Support is shut down concurrently (SLING-2704)
if (io == null || jspfh == null) {
logger.warn("callJsp: JSP Script Engine seems to be shut down concurrently; not calling {}",
scriptHelper.getScript().getScriptResource().getPath());
return;
}
final ResourceResolver oldResolver = io.setRequestResourceResolver(resolver);
jspfh.incUsage();
try {
final SlingBindings slingBindings = new SlingBindings();
slingBindings.putAll(bindings);
final JspServletWrapper jsp = getJspWrapper(scriptHelper, slingBindings);
// create a SlingBindings object
jsp.service(slingBindings);
} finally {
jspfh.decUsage();
io.resetRequestResourceResolver(oldResolver);
}
}
private JspServletWrapper getJspWrapper(final String scriptName, final SlingBindings bindings)
throws SlingException {
JspRuntimeContext rctxt = this.getJspRuntimeContext();
JspServletWrapper wrapper = rctxt.getWrapper(scriptName);
if (wrapper != null) {
if ( wrapper.isValid() ) {
return wrapper;
}
synchronized ( this ) {
rctxt = this.getJspRuntimeContext();
wrapper = rctxt.getWrapper(scriptName);
if ( wrapper != null ) {
if ( wrapper.isValid() ) {
return wrapper;
}
this.renewJspRuntimeContext();
rctxt = this.getJspRuntimeContext();
}
}
}
wrapper = new JspServletWrapper(servletConfig, options,
scriptName, false, rctxt, defaultIsSession);
wrapper = rctxt.addWrapper(scriptName, wrapper);
return wrapper;
}
private JspServletWrapper getJspWrapper(final SlingScriptHelper scriptHelper, final SlingBindings bindings)
throws SlingException {
final SlingScript script = scriptHelper.getScript();
final String scriptName = script.getScriptResource().getPath();
return getJspWrapper(scriptName, bindings);
}
// ---------- SCR integration ----------------------------------------------
/**
* Activate this component
*/
@Activate
protected void activate(final BundleContext bundleContext, final Map<String, Object> properties) {
this.defaultIsSession = PropertiesUtil.toBoolean(properties.get(PROP_DEFAULT_IS_SESSION), true);
// set the current class loader as the thread context loader for
// the setup of the JspRuntimeContext
final ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.dynamicClassLoader);
try {
this.jspFactoryHandler = JspRuntimeContext.initFactoryHandler();
this.tldLocationsCache = new SlingTldLocationsCache(bundleContext);
// prepare some classes
ioProvider = new SlingIOProvider(this.classLoaderWriter, this.javaCompiler);
// return options which use the jspClassLoader
options = new JspServletOptions(slingServletContext, ioProvider,
properties, tldLocationsCache);
jspServletContext = new JspServletContext(ioProvider,
slingServletContext, tldLocationsCache);
servletConfig = new JspServletConfig(jspServletContext, options.getProperties());
} finally {
// make sure the context loader is reset after setting up the
// JSP runtime context
Thread.currentThread().setContextClassLoader(old);
}
// check for changes in jasper config
this.checkJasperConfig();
logger.info("Activating Apache Sling Script Engine for JSP with options {}", options.getProperties());
logger.debug("IMPORTANT: Do not modify the generated servlet classes directly");
}
/**
* Activate this component
*/
@Deactivate
protected void deactivate(final BundleContext bundleContext) {
logger.info("Deactivating Apache Sling Script Engine for JSP");
if ( this.tldLocationsCache != null ) {
this.tldLocationsCache.deactivate(bundleContext);
this.tldLocationsCache = null;
}
if (jspRuntimeContext != null) {
this.destroyJspRuntimeContext(this.jspRuntimeContext);
jspRuntimeContext = null;
}
ioProvider = null;
this.jspFactoryHandler.destroy();
this.jspFactoryHandler = null;
}
private static final String CONFIG_PATH = "/jsp.config";
/**
* Check if the jasper configuration changed.
*/
private void checkJasperConfig() {
boolean changed = false;
InputStream is = null;
try {
is = this.classLoaderWriter.getInputStream(CONFIG_PATH);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length = 0;
while ( ( length = is.read(buffer)) != -1 ) {
baos.write(buffer, 0, length);
}
baos.close();
final String oldKey = new String(baos.toByteArray(), "UTF-8");
changed = !oldKey.equals(this.servletConfig.getConfigKey());
if ( changed ) {
logger.info("Removing all class files due to jsp configuration change");
}
} catch ( final IOException notFound ) {
changed = true;
} finally {
if ( is != null ) {
try {
is.close();
} catch ( final IOException ignore) {
// ignore
}
}
}
if ( changed ) {
OutputStream os = null;
try {
os = this.classLoaderWriter.getOutputStream(CONFIG_PATH);
os.write(this.servletConfig.getConfigKey().getBytes("UTF-8"));
} catch ( final IOException ignore ) {
// ignore
} finally {
if ( os != null ) {
try {
os.close();
} catch ( final IOException ignore ) {
// ignore
}
}
}
this.classLoaderWriter.delete("/org/apache/jsp");
}
}
/**
* Unbinds the Sling ServletContext and removes any known servlet context
* attributes preventing the bundles's class loader from being collected.
*
* @param slingServletContext The <code>ServletContext</code> to be unbound
*/
protected void unbindSlingServletContext(
final ServletContext slingServletContext) {
// remove JspApplicationContextImpl from the servlet context,
// otherwise a ClassCastException may be caused after this component
// is recreated because the class loader of the
// JspApplicationContextImpl class object is different from the one
// stored in the servlet context same for the AnnotationProcessor
// (which generally does not exist here)
try {
if (slingServletContext != null) {
slingServletContext.removeAttribute(JspApplicationContextImpl.class.getName());
slingServletContext.removeAttribute(AnnotationProcessor.class.getName());
}
} catch (NullPointerException npe) {
// SLING-530, might be thrown on system shutdown in a servlet
// container when using the Equinox servlet container bridge
logger.debug(
"unbindSlingServletContext: ServletContext might already be unavailable",
npe);
}
if (this.slingServletContext == slingServletContext) {
this.slingServletContext = null;
}
}
/**
* Bind the class load provider.
*
* @param repositoryClassLoaderProvider the new provider
*/
protected void bindDynamicClassLoaderManager(final DynamicClassLoaderManager rclp) {
if ( this.dynamicClassLoader != null ) {
this.ungetClassLoader();
}
this.getClassLoader(rclp);
}
/**
* Unbind the class loader provider.
* @param repositoryClassLoaderProvider the old provider
*/
protected void unbindDynamicClassLoaderManager(final DynamicClassLoaderManager rclp) {
if ( this.dynamicClassLoaderManager == rclp ) {
this.ungetClassLoader();
}
}
/**
* Get the class loader
*/
private void getClassLoader(final DynamicClassLoaderManager rclp) {
this.dynamicClassLoaderManager = rclp;
this.dynamicClassLoader = rclp.getDynamicClassLoader();
}
/**
* Unget the class loader
*/
private void ungetClassLoader() {
this.dynamicClassLoader = null;
this.dynamicClassLoaderManager = null;
}
// ---------- Internal -----------------------------------------------------
private class JspScriptEngine extends AbstractSlingScriptEngine {
JspScriptEngine() {
super(JspScriptEngineFactory.this);
}
@Override
public Object eval(final Reader script, final ScriptContext context)
throws ScriptException {
Bindings props = context.getBindings(ScriptContext.ENGINE_SCOPE);
SlingScriptHelper scriptHelper = (SlingScriptHelper) props.get(SLING);
if (scriptHelper != null) {
// set the current class loader as the thread context loader for
// the compilation and execution of the JSP script
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(dynamicClassLoader);
try {
callJsp(props, scriptHelper, context);
} catch (final SlingServletException e) {
// ServletExceptions use getRootCause() instead of getCause(),
// so we have to extract the actual root cause and pass it as
// cause in our new ScriptException
if (e.getCause() != null) {
// SlingServletException always wraps ServletExceptions
Throwable rootCause = TagUtil.getRootCause((ServletException) e.getCause());
// the ScriptException unfortunately does not accept a Throwable as cause,
// but only a Exception, so we have to wrap it with a dummy Exception in Throwable cases
if (rootCause instanceof Exception) {
throw new BetterScriptException(rootCause.toString(), (Exception) rootCause);
}
throw new BetterScriptException(rootCause.toString(),
new Exception("Wrapping Throwable: " + rootCause.toString(), rootCause));
}
// fallback to standard behaviour
throw new BetterScriptException(e.getMessage(), e);
} catch (final SlingPageException sje) {
callErrorPageJsp(props, scriptHelper, context, sje.getErrorPage());
} catch (final Exception e) {
throw new BetterScriptException(e.getMessage(), e);
} finally {
// make sure the context loader is reset after setting up the
// JSP runtime context
Thread.currentThread().setContextClassLoader(old);
}
}
return null;
}
}
private void destroyJspRuntimeContext(final JspRuntimeContext jrc) {
if (jrc != null) {
try {
jrc.destroy();
} catch (final NullPointerException npe) {
// SLING-530, might be thrown on system shutdown in a servlet
// container when using the Equinox servlet container bridge
logger.debug("deactivate: ServletContext might already be unavailable", npe);
}
}
}
private JspRuntimeContext getJspRuntimeContext() {
if ( this.jspRuntimeContext == null ) {
synchronized ( this ) {
if ( this.jspRuntimeContext == null ) {
// Initialize the JSP Runtime Context
this.jspRuntimeContext = new JspRuntimeContext(slingServletContext,
options, ioProvider);
}
}
}
return this.jspRuntimeContext;
}
/**
* Fixes {@link ScriptException} that overwrites the
* {@link ScriptException#getMessage()} method to display its own
* <code>message</code> instead of the <code>detailMessage</code>
* defined in {@link Throwable}. Unfortunately using the constructor
* {@link ScriptException#ScriptException(Exception)} does not set the
* <code>message</code> member of {@link ScriptException}, which leads to
* a message of <code>"null"</code>, effectively supressing the detailed
* information of the cause. This class provides a way to do that explicitly
* with a new constructor accepting both a message and a causing exception.
*
*/
private static class BetterScriptException extends ScriptException {
private static final long serialVersionUID = -6490165487977283019L;
public BetterScriptException(final String message, final Exception cause) {
super(message);
this.initCause(cause);
}
}
@Override
public void onChange(final List<ResourceChange> changes) {
for(final ResourceChange change : changes){
final JspRuntimeContext rctxt = this.jspRuntimeContext;
if ( rctxt != null && rctxt.handleModification(change.getPath(), change.getType() == ChangeType.REMOVED) ) {
renewJspRuntimeContext();
}
}
}
/**
* Renew the jsp runtime context.
* A new context is created, the old context is destroyed in the background
*/
private void renewJspRuntimeContext() {
final JspRuntimeContext jrc;
synchronized ( this ) {
jrc = this.jspRuntimeContext;
this.jspRuntimeContext = null;
}
final Thread t = new Thread() {
@Override
public void run() {
destroyJspRuntimeContext(jrc);
}
};
t.start();
}
//
// Web Console Plugin
//
private ServletConfig config;
/* (non-Javadoc)
* @see javax.servlet.Servlet#destroy()
*/
@Override
public void destroy() {
this.config = null;
}
/* (non-Javadoc)
* @see javax.servlet.Servlet#getServletConfig()
*/
@Override
public ServletConfig getServletConfig() {
return this.config;
}
/* (non-Javadoc)
* @see javax.servlet.Servlet#getServletInfo()
*/
@Override
public String getServletInfo() {
return "";
}
/* (non-Javadoc)
* @see javax.servlet.Servlet#init(javax.servlet.ServletConfig)
*/
@Override
public void init(final ServletConfig config) throws ServletException {
this.config = config;
}
/* (non-Javadoc)
* @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse)
*/
@Override
public void service(final ServletRequest request, final ServletResponse response)
throws ServletException, IOException {
if ( request instanceof HttpServletRequest ) {
final HttpServletRequest req = (HttpServletRequest) request;
final HttpServletResponse res = (HttpServletResponse) response;
final String path = req.getContextPath() + req.getServletPath() + req.getPathInfo();
if ( req.getMethod().equals("POST") ) {
final JspRuntimeContext rctxt = this.jspRuntimeContext;
this.classLoaderWriter.delete("/org/apache/jsp");
if ( rctxt != null ) {
renewJspRuntimeContext();
}
res.sendRedirect(path + "?reset");
return;
} else if ( req.getMethod().equals("GET") ) {
final PrintWriter pw = res.getWriter();
pw.println("<h1>Apache Sling JSP Scripting</h1>");
pw.println("<br/>");
if ( req.getParameter("reset") != null ) {
pw.println("<p>All compiled jsp files removed.");
pw.println("<br/>");
}
pw.print("<form action='");
pw.print(path);
pw.println("' method='POST'>");
pw.println("<input type='submit' value='Recompile all JSPs'>");
pw.println("</form>");
return;
}
}
throw new ServletException("Request not supported.");
}
}