/*
* 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.portals.bridges.script;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.portlet.ActionRequest;
import javax.portlet.ActionResponse;
import javax.portlet.EventPortlet;
import javax.portlet.EventRequest;
import javax.portlet.EventResponse;
import javax.portlet.GenericPortlet;
import javax.portlet.Portlet;
import javax.portlet.PortletConfig;
import javax.portlet.PortletException;
import javax.portlet.PreferencesValidator;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.ResourceRequest;
import javax.portlet.ResourceResponse;
import javax.portlet.ResourceServingPortlet;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
/**
* ScriptPortlet evaluates a script source to create a delegatee portlet instance
* and delegate invocations to the delegatee scripted portlet instance.
* <P>
* The final evaluated object from the scripted portlet source must be a portlet instance or portlet class
* based on the Java Portlet Specifications.
* </P>
* <P>
* Here's an example portlet definition with descriptions on init parameters.
* </P>
* <PRE><CODE><XMP>
* <portlet>
* <portlet-name>HelloGroovy</portlet-name>
* <display-name>Hello Groovy</display-name>
* <portlet-class>org.apache.portals.bridges.script.ScriptPortlet</portlet-class>
*
* <!-- Optional init parameter for script engine
* If this init parameter is not set, the ScriptPortlet will look up a script engine automatically
* by the mimeType or the extension of the script source file. -->
* <init-param>
* <name>engine</name>
* <!--
* Note: You can set other script engine which support JSR-223 ScriptEngine
* such as 'groovy', 'jruby', 'jython'.
* -->
* <value>groovy</value>
* </init-param>
*
* <!-- Optional init parameter for the key for portlet/preferencesValidator class or portlet/preferencesValidator instance which is evaluated and returned by the script.
* By default, when this init parameter is not set, ScriptPortlet retrieves the last evaluated object from the script.
* If you set this to 'value', then ScriptPortlet retrieves an object named 'value' from the bindings of the script engine.
* Depending on script engines, this init parameter should be properly configured because some script engines do not return the last evaluated object. -->
* <init-param>
* <name>eval-key</name>
* <value>value</value>
* </init-param>
*
* <!-- Required init parameter for script source path -->
* <init-param>
* <name>source</name>
* <!--
* Note: You can set script source in three ways.
* The first is to use context relative path,
* the second is to use file: url,
* and the third is to classpath: uri.
* Here are the examples for each way.
* -->
* <!--
* <value>/WEB-INF/groovy/HelloGroovy.groovy</value>
* <value>file:/C:/Program Files/Apache Software Foundation/Tomcat/webapps/demo/WEB-INF/groovy/HelloGroovy.groovy</value>
* <value>classpath:org/apache/portals/bridges/script/HelloGroovy.groovy</value>
* -->
* <value>classpath:org/apache/portals/bridges/script/HelloGroovy.groovy</value>
* </init-param>
*
* <!-- Optional init parameter for script file content encoding. The default value is 'UTF-8'. -->
* <init-param>
* <name>encoding</name>
* <value>UTF-8</value>
* </init-param>
*
* <!-- Optional init parameter for auto-refresh option.
* If auto-refresh is true, then a modification of script source can be refreshed automatically.
* By default, this option is set to false. -->
* <init-param>
* <name>auto-refresh</name>
* <value>true</value>
* </init-param>
*
* <!-- Optional init parameter for refresh-delay option.
* When auto-refresh is true, this init parameter sets the milliseconds of automatic refreshing interval.
* By default, this option is set to 60000 milliseconds (a minute). -->
* <init-param>
* <name>refresh-delay</name>
* <value>60000</value>
* </init-param>
*
* <!-- Optional init parameter for script preferences validator path -->
* <init-param>
* <name>validator</name>
* <!--
* Note: You can set script preferences validator source in three ways.
* The first is to use context relative path,
* the second is to use file: url,
* and the third is to classpath: uri.
* Here are the examples for each way.
* -->
* <!--
* <value>/WEB-INF/groovy/HelloGroovyPrefsValidator.groovy</value>
* <value>file:/C:/Program Files/Apache Software Foundation/Tomcat/webapps/demo/WEB-INF/groovy/HelloGroovyPrefsValidator.groovy</value>
* <value>classpath:org/apache/portals/bridges/script/HelloGroovyPrefsValidator.groovy</value>
* -->
* <value>classpath:org/apache/portals/bridges/script/HelloGroovyPrefsValidator.groovy</value>
* </init-param>
*
* <!-- The followings are not special for ScriptPortlet, but normal configurations for a portlet. -->
* <supports>
* <mime-type>text/html</mime-type>
* <portlet-mode>VIEW</portlet-mode>
* <portlet-mode>EDIT</portlet-mode>
* <portlet-mode>HELP</portlet-mode>
* </supports>
* <supported-locale>en</supported-locale>
* <portlet-info>
* <title>Hello Groovy</title>
* <short-title>Hello Groovy</short-title>
* <keywords>demo,groovy</keywords>
* </portlet-info>
* <portlet-preferences>
* <preference>
* <name>message</name>
* <value>Hello, Groovy!</value>
* </preference>
* <preferences-validator>org.apache.portals.bridges.script.ScriptPortletPreferencesValidator</preferences-validator>
* </portlet-preferences>
*
* </portlet>
* </XMP></CODE></PRE>
*
* @author <a href="mailto:woonsan@apache.org">Woonsan Ko</a>
* @version $Id: ScriptPortlet.java 937248 2010-04-23 11:06:13Z woonsan $
*/
public class ScriptPortlet extends GenericPortlet
{
public static final String ENGINE = "engine";
public static final String EVAL_KEY = "eval-key";
public static final String SOURCE = "source";
public static final String VALIDATOR = "validator";
public static final String URI_ENCODING = "uri-encoding";
public static final String ENCODING = "encoding";
public static final String AUTO_REFRESH = "auto-refresh";
public static final String REFRESH_DELAY = "refresh-delay";
public static final String SCRIPT_SOURCE_FACTORY = "script-source-factory";
private PortletConfig portletConfig;
private String scriptEngineName;
private String evalKey;
private ScriptEngine scriptEngine;
private String scriptSourceUri;
private String scriptSourceUriEncoding;
private String scriptSourceCharacterEncoding;
private boolean autoRefresh;
private long refreshDelay = 60000L;
private ScriptSource scriptSource;
private long scriptSourceLastEvalStarted;
private long scriptSourceLastEvaluated;
private long scriptSourceLastModified;
private Portlet scriptPortletInstance;
private GenericPortlet scriptGenericPortletInstance;
private Method portletDoEditMethod;
private String validatorSourceUri;
private ScriptSource validatorSource;
private long validatorSourceLastEvalStarted;
private long validatorSourceLastEvaluated;
private long validatorSourceLastModified;
private PreferencesValidator validatorInstance;
private static ThreadLocal<PreferencesValidator> tlCurrentValidatorInstance = new ThreadLocal<PreferencesValidator>();
private ScriptSourceFactory scriptSourceFactory = new SimpleScriptSourceFactory();
public ScriptPortlet()
{
super();
}
@Override
public void init(PortletConfig config) throws PortletException
{
portletConfig = config;
String param = config.getInitParameter(ENGINE);
if (param != null && !"".equals(param.trim()))
{
scriptEngineName = param;
}
param = config.getInitParameter(EVAL_KEY);
if (param != null && !"".equals(param.trim()))
{
evalKey = param;
}
autoRefresh = "true".equals(config.getInitParameter(AUTO_REFRESH));
param = config.getInitParameter(REFRESH_DELAY);
if (param != null && !"".equals(param.trim()))
{
refreshDelay = Long.parseLong(param.trim());
}
param = config.getInitParameter(SCRIPT_SOURCE_FACTORY);
if (param != null && !"".equals(param.trim()))
{
try
{
scriptSourceFactory = (ScriptSourceFactory) Thread.currentThread().getContextClassLoader().loadClass(param).newInstance();
}
catch (Exception e)
{
throw new PortletException("Configuration failed: " + SCRIPT_SOURCE_FACTORY + ". Cannot create script source factory: " + param);
}
}
param = config.getInitParameter(ENCODING);
if (param != null && !"".equals(param.trim()))
{
scriptSourceCharacterEncoding = param;
}
param = config.getInitParameter(URI_ENCODING);
if (param != null && !"".equals(param.trim()))
{
scriptSourceUriEncoding = param;
}
scriptSourceUri = config.getInitParameter(SOURCE);
if (scriptSourceUri == null)
{
throw new PortletException("Configuration failed: " + SOURCE + " should be set properly!");
}
else
{
try
{
if (scriptSourceUri.startsWith("/"))
{
scriptSource = scriptSourceFactory.createScriptSource(new File(config.getPortletContext().getRealPath(scriptSourceUri)).toURL().toString(), scriptSourceUriEncoding, scriptSourceCharacterEncoding);
}
else
{
scriptSource = scriptSourceFactory.createScriptSource(scriptSourceUri, scriptSourceUriEncoding, scriptSourceCharacterEncoding);
}
scriptSource.setMimeType(config.getPortletContext().getMimeType(scriptSource.getName()));
}
catch (Exception e)
{
throw new PortletException("Script not found: " + this.scriptSourceUri, e);
}
}
refreshPortletInstance();
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
validatorSourceUri = config.getInitParameter(VALIDATOR);
if (validatorSourceUri != null)
{
try
{
if (validatorSourceUri.startsWith("/"))
{
validatorSource = scriptSourceFactory.createScriptSource(new File(config.getPortletContext().getRealPath(validatorSourceUri)).toURL().toString(), scriptSourceUriEncoding, scriptSourceCharacterEncoding);
}
else
{
validatorSource = scriptSourceFactory.createScriptSource(validatorSourceUri, scriptSourceUriEncoding, scriptSourceCharacterEncoding);
}
validatorSource.setMimeType(config.getPortletContext().getMimeType(validatorSource.getName()));
}
catch (Exception e)
{
throw new PortletException("Script not found: " + validatorSourceUri, e);
}
refreshValidatorInstance();
}
}
@Override
public void destroy()
{
if (scriptPortletInstance != null)
{
scriptPortletInstance.destroy();
}
}
@Override
public PortletConfig getPortletConfig ()
{
return portletConfig;
}
@Override
public void render(RenderRequest request, RenderResponse response) throws PortletException, IOException
{
refreshPortletInstance();
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
scriptPortletInstance.render(request, response);
}
@Override
public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException
{
refreshPortletInstance();
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
else
{
try
{
refreshValidatorInstance();
if (validatorInstance != null)
{
tlCurrentValidatorInstance.set(validatorInstance);
}
scriptPortletInstance.processAction(request, response);
}
finally
{
if (validatorInstance != null)
{
tlCurrentValidatorInstance.set(null);
}
}
}
}
@Override
public void processEvent(EventRequest request, EventResponse response) throws PortletException, IOException
{
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
if (scriptPortletInstance instanceof EventPortlet)
{
((EventPortlet) scriptPortletInstance).processEvent(request, response);
}
}
@Override
public void serveResource(ResourceRequest request, ResourceResponse response) throws PortletException, IOException
{
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
if (scriptPortletInstance instanceof ResourceServingPortlet)
{
((ResourceServingPortlet) scriptPortletInstance).serveResource(request, response);
}
}
@Override
public void doEdit(RenderRequest request, RenderResponse response) throws PortletException, IOException
{
if (scriptPortletInstance == null)
{
throw new PortletException("Script portlet is not available!");
}
if (scriptGenericPortletInstance != null && portletDoEditMethod != null)
{
try
{
portletDoEditMethod.invoke(scriptGenericPortletInstance, new Object [] { request, response });
}
catch (Exception e)
{
throw new PortletException("Failed to invoke doEdit method.", e);
}
}
else
{
throw new PortletException("doEdit method not implemented or not public.");
}
}
protected void refreshPortletInstance() throws PortletException
{
boolean scriptCreated = (scriptPortletInstance != null);
boolean scriptModified = false;
long checkedScriptSourceLastModified = 0L;
if (scriptCreated)
{
if (autoRefresh)
{
if ((refreshDelay <= 0L) || (System.currentTimeMillis() - scriptSourceLastEvalStarted > refreshDelay))
{
scriptModified = (ScriptEngineUtils.isScriptModified(scriptSource, scriptSourceLastModified));
}
}
if (scriptModified)
{
checkedScriptSourceLastModified = scriptSource.lastModified();
}
else
{
return;
}
}
try
{
synchronized (this)
{
if ((scriptModified && scriptSourceLastEvaluated >= checkedScriptSourceLastModified) || (!scriptCreated && scriptPortletInstance != null))
{
return;
}
scriptSourceLastModified = checkedScriptSourceLastModified;
scriptSourceLastEvalStarted = System.currentTimeMillis();
Portlet tempScriptPortletInstance = null;
if (scriptEngine == null)
{
scriptEngine = ScriptEngineUtils.createScriptEngine(scriptEngineName, scriptSource);
}
Object evalPortlet = ScriptEngineUtils.evaluateScript(scriptEngine, scriptSource, evalKey, true);
scriptSourceLastEvaluated = System.currentTimeMillis();
if (evalPortlet instanceof Portlet)
{
tempScriptPortletInstance = (Portlet) evalPortlet;
}
else if (evalPortlet instanceof Class)
{
Class<? extends Portlet> scriptPortletClass = (Class<? extends Portlet>) evalPortlet;
tempScriptPortletInstance = (Portlet) scriptPortletClass.newInstance();
}
else
{
throw new ScriptException("The evaluated return is neither class nor instance of javax.portlet.Portlet. " + evalPortlet);
}
scriptGenericPortletInstance = null;
portletDoEditMethod = null;
if (tempScriptPortletInstance instanceof GenericPortlet)
{
scriptGenericPortletInstance = (GenericPortlet) tempScriptPortletInstance;
try
{
Method doEditMethod = scriptGenericPortletInstance.getClass().getMethod("doEdit", new Class [] { RenderRequest.class, RenderResponse.class });
if (Modifier.isPublic(doEditMethod.getModifiers()))
{
portletDoEditMethod = doEditMethod;
}
}
catch (NoSuchMethodException e)
{
}
}
tempScriptPortletInstance.init(portletConfig);
scriptPortletInstance = tempScriptPortletInstance;
}
}
catch (Exception ex)
{
throw new PortletException("Could not evaluate script: " + scriptSourceUri, ex);
}
}
protected void refreshValidatorInstance() throws PortletException
{
if (validatorSource == null)
{
return;
}
boolean scriptCreated = (validatorInstance != null);
boolean scriptModified = false;
long checkedScriptSourceLastModified = 0L;
if (scriptCreated)
{
if (autoRefresh)
{
if ((refreshDelay <= 0L) || (System.currentTimeMillis() - validatorSourceLastEvalStarted > refreshDelay))
{
scriptModified = (ScriptEngineUtils.isScriptModified(validatorSource, validatorSourceLastModified));
}
}
if (scriptModified)
{
checkedScriptSourceLastModified = validatorSource.lastModified();
}
else
{
return;
}
}
try
{
synchronized (this)
{
if ((scriptModified && validatorSourceLastEvaluated >= checkedScriptSourceLastModified) || (!scriptCreated && validatorInstance != null))
{
return;
}
validatorSourceLastModified = checkedScriptSourceLastModified;
validatorSourceLastEvalStarted = System.currentTimeMillis();
PreferencesValidator tempValidatorInstance = null;
Object evalPortlet = ScriptEngineUtils.evaluateScript(scriptEngine, validatorSource, evalKey, true);
validatorSourceLastEvaluated = System.currentTimeMillis();
if (evalPortlet instanceof PreferencesValidator)
{
tempValidatorInstance = (PreferencesValidator) evalPortlet;
}
else if (evalPortlet instanceof Class)
{
Class<? extends PreferencesValidator> validatorClass = (Class<? extends PreferencesValidator>) evalPortlet;
tempValidatorInstance = (PreferencesValidator) validatorClass.newInstance();
}
else
{
throw new ScriptException("The evaluated return is neither class nor instance of javax.portlet.PreferencesValidator. " + evalPortlet);
}
validatorInstance = tempValidatorInstance;
}
}
catch (Exception ex)
{
throw new PortletException("Could not evaluate script: " + validatorSourceUri, ex);
}
}
protected Portlet getScriptPortletInstance()
{
return scriptPortletInstance;
}
protected long getScriptSourceLastEvaluated()
{
return scriptSourceLastEvaluated;
}
protected long setRefreshDelay()
{
return refreshDelay;
}
protected void setRefreshDelay(long refreshDelay)
{
this.refreshDelay = refreshDelay;
}
protected ScriptSource getScriptSource()
{
return scriptSource;
}
protected ScriptSource getValidatorSource()
{
return validatorSource;
}
protected PreferencesValidator getValidatorInstance()
{
return validatorInstance;
}
protected ScriptSourceFactory getScriptSourceFactory()
{
return scriptSourceFactory;
}
protected void setScriptSourceFactory(ScriptSourceFactory scriptSourceFactory)
{
this.scriptSourceFactory = scriptSourceFactory;
}
protected static PreferencesValidator getCurrentValidatorInstance()
{
return (PreferencesValidator) tlCurrentValidatorInstance.get();
}
}