blob: 09e9a9f738cdb32ed64d7cf5c9b860d2c92969f7 [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.Reader;
import javax.jcr.RepositoryException;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingIOException;
import org.apache.sling.api.SlingServletException;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.scripting.SlingScript;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.classloader.RepositoryClassLoaderProvider;
import org.apache.sling.scripting.api.AbstractScriptEngineFactory;
import org.apache.sling.scripting.api.AbstractSlingScriptEngine;
import org.apache.sling.scripting.jsp.jasper.JasperException;
import org.apache.sling.scripting.jsp.jasper.Options;
import org.apache.sling.scripting.jsp.jasper.compiler.JspRuntimeContext;
import org.apache.sling.scripting.jsp.jasper.runtime.JspApplicationContextImpl;
import org.apache.sling.scripting.jsp.util.TagUtil;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The JSP engine (a.k.a Jasper).
*
* @scr.component label="%jsphandler.name" description="%jsphandler.description"
* @scr.property name="service.description" value="JSP Script Handler"
* @scr.property name="service.vendor" value="The Apache Software Foundation" *
* @scr.property name="jasper.checkInterval" value="300" type="Integer"
* @scr.property name="jasper.classdebuginfo" value="true" type="Boolean"
* @scr.property name="jasper.development" value="true" type="Boolean"
* @scr.property name="jasper.enablePooling" value="true" type="Boolean"
* @scr.property name="jasper.ieClassId"
* value="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93"
* @scr.property name="jasper.genStringAsCharArray" value="false" type="Boolean"
* @scr.property name="jasper.keepgenerated" value="true" type="Boolean"
* @scr.property name="jasper.mappedfile" value="true" type="Boolean"
* @scr.property name="jasper.modificationTestInterval" value="4" type="Integer"
* @scr.property name="jasper.reloading" value="false" type="Boolean"
* @scr.property name="jasper.scratchdir" value="/var/classes"
* @scr.property name="jasper.trimSpaces" value="false" type="Boolean"
* @scr.property name="jasper.displaySourceFragments" value="true"
* type="Boolean"
* @scr.service
*/
public class JspScriptEngineFactory extends AbstractScriptEngineFactory {
/** default log */
private static final Logger log = LoggerFactory.getLogger(JspScriptEngineFactory.class);
ComponentContext componentContext;
/** @scr.reference */
private SlingRepository repository;
/** @scr.reference */
private ServletContext slingServletContext;
/**
* @scr.reference name="RepositoryClassLoaderProvider"
* interface="org.apache.sling.jcr.classloader.RepositoryClassLoaderProvider"
*/
private ClassLoader jspClassLoader;
private SlingIOProvider ioProvider;
private SlingTldLocationsCache tldLocationsCache;
private JspRuntimeContext jspRuntimeContext;
private Options options;
private JspServletContext jspServletContext;
private ServletConfig servletConfig;
private RepositoryClassLoaderProvider repoCLProvider;
public static final String[] SCRIPT_TYPE = { "jsp", "jspf", "jspx" };
private static final String CLASSLOADER_NAME = "admin";
public JspScriptEngineFactory() {
setExtensions(SCRIPT_TYPE);
}
public ScriptEngine getScriptEngine() {
return new JspScriptEngine();
}
public String getLanguageName() {
return "Java Server Pages";
}
public String getLanguageVersion() {
return "2.1";
}
/**
* @param scriptHelper
* @throws SlingServletException
* @throws SlingIOException
*/
private void callJsp(Bindings bindings, SlingScriptHelper scriptHelper) {
ioProvider.setRequestResourceResolver(scriptHelper.getRequest().getResourceResolver());
try {
JspServletWrapperAdapter jsp = getJspWrapperAdapter(scriptHelper);
// create a SlingBindings object
final SlingBindings slingBindings = new SlingBindings();
slingBindings.putAll(bindings);
jsp.service(slingBindings);
} finally {
ioProvider.resetRequestResourceResolver();
}
}
private JspServletWrapperAdapter getJspWrapperAdapter(
SlingScriptHelper scriptHelper) throws SlingException {
JspRuntimeContext rctxt = jspRuntimeContext;
SlingScript script = scriptHelper.getScript();
String scriptName = script.getScriptResource().getPath();
JspServletWrapperAdapter wrapper = (JspServletWrapperAdapter) rctxt.getWrapper(scriptName);
if (wrapper != null) {
return wrapper;
}
synchronized (this) {
wrapper = (JspServletWrapperAdapter) rctxt.getWrapper(scriptName);
if (wrapper != null) {
return wrapper;
}
try {
wrapper = new JspServletWrapperAdapter(servletConfig, options,
scriptName, false, rctxt);
rctxt.addWrapper(scriptName, wrapper);
return wrapper;
} catch (JasperException je) {
if (je.getCause() != null) {
throw new SlingException(je.getMessage(), je.getCause());
}
throw new SlingException("Cannot create JSP", je);
}
}
}
// ---------- SCR integration ----------------------------------------------
protected void activate(ComponentContext componentContext) {
this.componentContext = componentContext;
// set the current class loader as the thread context loader for
// the setup of the JspRuntimeContext
ClassLoader old = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(jspClassLoader);
try {
// prepare some classes
prepareJasperClasses();
ioProvider = new SlingIOProvider(repository, slingServletContext);
tldLocationsCache = new SlingTldLocationsCache(slingServletContext,
componentContext.getBundleContext());
// return options which use the jspClassLoader
options = new JspServletOptions(slingServletContext, ioProvider,
componentContext, jspClassLoader, tldLocationsCache);
// Initialize the JSP Runtime Context
jspRuntimeContext = new JspRuntimeContext(slingServletContext,
options);
// by default access the repository
jspRuntimeContext.setIOProvider(ioProvider);
jspServletContext = new JspServletContext(ioProvider,
slingServletContext, tldLocationsCache);
servletConfig = new JspServletConfig(jspServletContext,
componentContext.getProperties());
} finally {
// make sure the context loader is reset after setting up the
// JSP runtime context
Thread.currentThread().setContextClassLoader(old);
}
if (log.isDebugEnabled()) {
log.debug("Scratch dir for the JSP engine is: {}",
options.getScratchDir().toString());
log.debug("IMPORTANT: Do not modify the generated servlets");
}
}
protected void deactivate(ComponentContext oldComponentContext) {
if (log.isDebugEnabled()) {
log.debug("JspScriptEngine.deactivate()");
}
if (jspRuntimeContext != null) {
try {
jspRuntimeContext.destroy();
} catch (NullPointerException npe) {
// SLING-530, might be thrown on system shutdown in a servlet
// container when using the Equinox servlet container bridge
log.debug("deactivate: ServletContext might already be unavailable", npe);
}
jspRuntimeContext = null;
}
if (tldLocationsCache != null) {
tldLocationsCache.shutdown(componentContext.getBundleContext());
tldLocationsCache = null;
}
ioProvider = null;
componentContext = null;
// 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
try {
slingServletContext.removeAttribute(JspApplicationContextImpl.class.getName());
} catch (NullPointerException npe) {
// SLING-530, might be thrown on system shutdown in a servlet
// container when using the Equinox servlet container bridge
log.debug("deactivate: ServletContext might already be unavailable", npe);
}
}
/**
* Bind the class load provider.
* @param repositoryClassLoaderProvider the new provider
*/
protected void bindRepositoryClassLoaderProvider(RepositoryClassLoaderProvider rclp) {
if ( this.jspClassLoader != null ) {
this.ungetClassLoader();
}
this.getClassLoader(rclp);
}
/**
* Unbind the class loader provider.
* @param repositoryClassLoaderProvider the old provider
*/
protected void unbindRepositoryClassLoaderProvider(RepositoryClassLoaderProvider rclp) {
if ( this.repoCLProvider == rclp ) {
this.ungetClassLoader();
}
}
/**
* Get the class loader
*/
private void getClassLoader(RepositoryClassLoaderProvider rclp) {
try {
this.repoCLProvider = rclp;
this.jspClassLoader = rclp.getClassLoader(CLASSLOADER_NAME);
} catch (RepositoryException re) {
log.error("Cannot get JSP class loader", re);
}
}
/**
* Unget the class loader
*/
private void ungetClassLoader() {
if ( this.repoCLProvider != null ) {
if ( this.jspClassLoader != null ) {
this.repoCLProvider.ungetClassLoader(this.jspClassLoader);
this.jspClassLoader = null;
}
this.repoCLProvider = null;
}
}
private void prepareJasperClasses() {
final String propName = "org.apache.sling.scripting.jsp.jasper.runtime.JspFactoryImpl.USE_POOL";
final String propValue = System.getProperty(propName);
try {
// hacky wacky to prevent PageContext pooling !!!
System.setProperty(propName, "false");
jspClassLoader.loadClass("org.apache.sling.scripting.jsp.jasper.runtime.JspFactoryImpl");
} catch (Throwable t) {
// don't care for now
} finally {
if (propValue != null) {
System.setProperty(propName, propValue);
} else {
System.clearProperty(propName);
}
}
}
// ---------- Internal -----------------------------------------------------
private class JspScriptEngine extends AbstractSlingScriptEngine {
JspScriptEngine() {
super(JspScriptEngineFactory.this);
}
public Object eval(Reader script, 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(jspClassLoader);
try {
callJsp(props, scriptHelper);
} catch (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);
} else {
throw new BetterScriptException(rootCause.toString(),
new Exception("Wrapping Throwable: " + rootCause.toString(), rootCause));
}
}
// fallback to standard behaviour
throw new BetterScriptException(e.getMessage(), e);
} catch (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;
}
}
/**
* 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 {
public BetterScriptException(String message, Exception cause) {
super(message);
this.initCause(cause);
}
}
}