/*
* Copyright 2005 Joe Walker
*
* 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.directwebremoting.impl;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.directwebremoting.AjaxFilter;
import org.directwebremoting.AjaxFilterChain;
import org.directwebremoting.WebContext;
import org.directwebremoting.WebContextFactory;
import org.directwebremoting.extend.AccessControl;
import org.directwebremoting.extend.AjaxFilterManager;
import org.directwebremoting.extend.Call;
import org.directwebremoting.extend.Calls;
import org.directwebremoting.extend.Converter;
import org.directwebremoting.extend.ConverterManager;
import org.directwebremoting.extend.Creator;
import org.directwebremoting.extend.CreatorManager;
import org.directwebremoting.extend.EnginePrivate;
import org.directwebremoting.extend.NamedConverter;
import org.directwebremoting.extend.Property;
import org.directwebremoting.extend.Remoter;
import org.directwebremoting.extend.Replies;
import org.directwebremoting.extend.Reply;
import org.directwebremoting.filter.LogAjaxFilter;
import org.directwebremoting.util.Continuation;
import org.directwebremoting.util.JavascriptUtil;
import org.directwebremoting.util.LocalUtil;
/**
* In implementation of Remoter that delegates requests to a set of Modules
* @author Joe Walker [joe at getahead dot ltd dot uk]
* @author Mike Wilson
*/
public class DefaultRemoter implements Remoter
{
/* (non-Javadoc)
* @see org.directwebremoting.Remoter#generateInterfaceScript(java.lang.String, java.lang.String)
*/
public String generateInterfaceScript(String scriptName, String contextServletPath) throws SecurityException
{
StringBuilder buffer = new StringBuilder();
buffer.append(createParameterDefinitions(scriptName));
buffer.append(EnginePrivate.getEngineInitScript());
buffer.append(createClassDefinition(scriptName));
buffer.append(createPathDefinition(scriptName, contextServletPath));
buffer.append(createMethodDefinitions(scriptName));
return buffer.toString();
}
/**
* Create a class definition string.
* This is similar to {@link EnginePrivate#getEngineInitScript()} except
* that it creates scripts for a specific class not for dwr.engine
* @see EnginePrivate#getEngineInitScript()
* @param scriptName
*/
protected String createClassDefinition(String scriptName)
{
return "if (typeof this['" + scriptName + "'] == 'undefined') this." + scriptName + " = {};\n\n";
}
/**
* Create a _path member to point at DWR
* @param scriptName The class that we are creating a member for
* @param path The default path to the DWR servlet
*/
protected String createPathDefinition(String scriptName, String path)
{
return scriptName + "._path = '" + getPathToDwrServlet(path) + "';\n\n";
}
/* (non-Javadoc)
* @see org.directwebremoting.extend.Remoter#getPathToDwrServlet(java.lang.String)
*/
public String getPathToDwrServlet(String contextServletPath)
{
String actualPath = contextServletPath;
if (overridePath != null)
{
actualPath = overridePath;
}
if (useAbsolutePath)
{
HttpServletRequest request = WebContextFactory.get().getHttpServletRequest();
StringBuffer absolutePath = new StringBuffer(48);
String scheme = request.getScheme();
int port = request.getServerPort();
absolutePath.append(scheme);
absolutePath.append("://");
absolutePath.append(request.getServerName());
if (port > 0 &&
(("http".equalsIgnoreCase(scheme) && port != 80) ||
("https".equalsIgnoreCase(scheme) && port != 443)))
{
absolutePath.append(':');
absolutePath.append(port);
}
absolutePath.append(request.getContextPath());
absolutePath.append(request.getServletPath());
actualPath = absolutePath.toString();
}
return actualPath;
}
/**
* Create a list of method definitions for the given creator.
* @param fullCreatorName To allow AccessControl to allow/deny requests
*/
protected String createMethodDefinitions(String fullCreatorName)
{
Creator creator = creatorManager.getCreator(fullCreatorName, false);
String scriptName = creator.getJavascript();
StringBuilder buffer = new StringBuilder();
Method[] methods = creator.getType().getMethods();
for (Method method : methods)
{
String methodName = method.getName();
// We don't need to check accessControl.getReasonToNotExecute()
// because the checks are made by the execute() method, but we do
// check if we can display it
try
{
accessControl.assertIsDisplayable(creator, scriptName, method);
}
catch (SecurityException ex)
{
if (!allowImpossibleTests)
{
continue;
}
}
// Is it on the list of banned names
if (JavascriptUtil.isReservedWord(methodName))
{
continue;
}
// Check to see if the creator is reloadable
// If it is, then do not cache the generated Javascript
// See the notes on creator.isCacheable().
String script;
if (!creator.isCacheable())
{
script = getMethodJS(scriptName, method);
}
else
{
String key = scriptName + "." + method.getName();
// For optimal performance we might use the Memoizer pattern
// JCiP#108 however performance isn't a big issue and we are
// prepared to cope with getMethodJS() being run more than once.
script = methodCache.get(key);
if (script == null)
{
script = getMethodJS(scriptName, method);
methodCache.put(key, script);
}
}
buffer.append(script);
}
return buffer.toString();
}
private StringBuilder createParameterDefinition(String clazz, String jsClassName, NamedConverter boConv)
{
Class<?> mappedType;
StringBuilder paramBuffer = new StringBuilder();
try
{
mappedType = LocalUtil.classForName(clazz);
}
catch (ClassNotFoundException ex)
{
throw new IllegalArgumentException(ex.getMessage());
}
if (!mappedType.isInterface())
{
String[] parts = jsClassName.split("\\.");
if (parts.length == 1)
{
paramBuffer.append("\nif (typeof this['").append(jsClassName).append("'] != 'function') {\n").append(" function ").append(jsClassName).append("() {\n");
}
else
{
for (int i = 0; i < parts.length - 1; i++)
{
paramBuffer.append("\nif (typeof ");
for (int j = 0; j <= i; j++)
paramBuffer.append(parts[j]).append(".");
paramBuffer.deleteCharAt(paramBuffer.length() - 1).append(" == \"undefined\") ");
if (i == 0)
paramBuffer.append("var ");
for (int j = 0; j <= i; j++)
paramBuffer.append(parts[j]).append(".");
paramBuffer.deleteCharAt(paramBuffer.length() - 1).append(" = {};\n");
}
paramBuffer.append("\nif (typeof ").append(jsClassName).append(" != 'function') {\n ").append(jsClassName).append(" = function ").append(parts[parts.length - 1]).append(" () {\n");
}
Map<String, Property> properties = boConv.getPropertyMapFromClass(mappedType, true, true);
for (Entry<String, Property> entry : properties.entrySet())
{
String name = entry.getKey();
Property property = entry.getValue();
Class<?> propType = property.getPropertyType();
paramBuffer.append(" this.").append(name).append(" = ");
if (propType.isArray() || List.class.isAssignableFrom(propType))
paramBuffer.append("[];\n");
else if (propType == boolean.class)
paramBuffer.append("false;\n");
else if (propType.isPrimitive())
paramBuffer.append("0;\n");
else
paramBuffer.append("null;\n");
}
paramBuffer.append(" }\n").append("}\n");
}
return paramBuffer;
}
/**
* Output the class definitions for all the converted objects.
* An optimization for this class might be to only generate class
* definitions for classes used as parameters in the class that we are
* currently generating a proxy for.
* <p>Currently the <code>scriptName</code> parameter is not used, we just
* generate the class definitions for all types, however conceptually, it
* should be used
* @param scriptName The script for which we are generating parameter classes
*/
protected String createParameterDefinitions(String scriptName) {
StringBuilder buffer = new StringBuilder();
for (String match : converterManager.getConverterMatchStrings()) {
try {
Converter conv = converterManager.getConverterByMatchString(match);
if (conv instanceof NamedConverter) {
NamedConverter boConv = (NamedConverter) conv;
String jsClassName = boConv.getJavascript();
if (jsClassName != null && !"".equals(jsClassName)) {
if (!match.contains("*")) {
if ("*".equals(jsClassName)) jsClassName = match.substring(match.lastIndexOf('.') + 1);
if ("**".equals(jsClassName)) jsClassName = match;
buffer.append(createParameterDefinition(match, jsClassName, boConv));
} else {
ClasspathScanner scanner = new ClasspathScanner(match);
for (String clazz : scanner.getClasses()) {
buffer.append(createParameterDefinition(clazz, "*".equals(jsClassName) ? clazz.substring(clazz.lastIndexOf('.') + 1) : clazz, boConv));
}
}
}
}
} catch (Exception ex) {
log.warn("Failed to create parameter declaration for " + match, ex);
buffer.append("// Missing parameter declaration for ").append(match).append(". See the server logs for details.");
}
}
return buffer.append('\n').toString();
}
/**
* Generates Javascript for a given Java method
* @param scriptName Name of the Javascript file, without ".js" suffix
* @param method Target method
* @return Javascript implementing the DWR call for the target method
*/
protected String getMethodJS(String scriptName, Method method)
{
StringBuffer buffer = new StringBuffer();
String methodName = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
// Create the sdoc comment
buffer.append("/**\n");
for (int j = 0; j < paramTypes.length; j++)
{
if (!LocalUtil.isServletClass(paramTypes[j]))
{
buffer.append(" * @param {" + paramTypes[j] + "} p" + j + " a param\n");
}
}
buffer.append(" * @param {function|Object} callback callback function or options object\n");
buffer.append(" */\n");
// Create the function definition
buffer.append(scriptName + '.' + methodName + " = function(");
for (int j = 0; j < paramTypes.length; j++)
{
if (!LocalUtil.isServletClass(paramTypes[j]))
{
buffer.append("p" + j + ", ");
}
}
buffer.append("callback) {\n");
// The method body calls into engine.js
String executeFunctionName = EnginePrivate.getExecuteFunctionName();
buffer.append(" return " + executeFunctionName + "(" + scriptName + "._path, '" + scriptName + "', '" + methodName + "\', ");
for (int j = 0; j < paramTypes.length; j++)
{
if (LocalUtil.isServletClass(paramTypes[j]))
{
buffer.append("false, ");
}
else
{
buffer.append("p" + j + ", ");
}
}
buffer.append("callback);\n");
buffer.append("};\n\n");
return buffer.toString();
}
/* (non-Javadoc)
* @see org.directwebremoting.Remoter#execute(org.directwebremoting.Calls)
*/
public Replies execute(Calls calls)
{
Replies replies = new Replies(calls.getBatchId());
int callCount = calls.getCallCount();
if (callCount > maxCallCount)
{
log.error("Call count for batch exceeds maxCallCount. Add an init-param of maxCallCount to increase this limit");
throw new SecurityException("Call count for batch is too high");
}
for (Call call : calls)
{
Reply reply = execute(call);
replies.addReply(reply);
}
return replies;
}
/**
* Execute a single call object
* @param call The call to execute
* @return A Reply to the Call
*/
public Reply execute(Call call)
{
// We set this up here because if something goes wrong we want to know
// if there are any LogAjaxFilter implementations to provide any logging
List<AjaxFilter> filters = ajaxFilterManager.getAjaxFilters(call.getScriptName());
try
{
Method method = call.getMethod();
if (method == null || call.getException() != null)
{
return new Reply(call.getCallId(), null, call.getException());
}
// Get a list of the available matching methods with the coerced
// parameters that we will use to call it if we choose to use that
// method.
Creator creator = creatorManager.getCreator(call.getScriptName(), true);
// We don't need to check accessControl.getReasonToNotExecute()
// because the checks are made by the doExec method, but we do check
// if we can display it
accessControl.assertExecutionIsPossible(creator, call.getScriptName(), method);
// Get ourselves an object to execute a method on unless the
// method is static
Object object = null;
String scope = creator.getScope();
boolean create = false;
if (!Modifier.isStatic(method.getModifiers()))
{
WebContext webcx = WebContextFactory.get();
// Check the various scopes to see if it is there
if (scope.equals(Creator.APPLICATION))
{
object = webcx.getServletContext().getAttribute(call.getScriptName());
}
else if (scope.equals(Creator.SESSION))
{
object = webcx.getSession().getAttribute(call.getScriptName());
}
else if (scope.equals(Creator.SCRIPT))
{
object = webcx.getScriptSession().getAttribute(call.getScriptName());
}
else if (scope.equals(Creator.REQUEST))
{
object = webcx.getHttpServletRequest().getAttribute(call.getScriptName());
}
// Creator.PAGE scope means we create one every time anyway
// If we don't have an object the call the creator
if (object == null)
{
create = true;
object = creator.getInstance();
}
// Remember it for next time
if (create)
{
if (scope.equals(Creator.APPLICATION))
{
// This might also be done at application startup by
// DefaultCreatorManager.addCreator(String, Creator)
webcx.getServletContext().setAttribute(call.getScriptName(), object);
}
else if (scope.equals(Creator.SESSION))
{
webcx.getSession().setAttribute(call.getScriptName(), object);
}
else if (scope.equals(Creator.SCRIPT))
{
webcx.getScriptSession().setAttribute(call.getScriptName(), object);
}
else if (scope.equals(Creator.REQUEST))
{
webcx.getHttpServletRequest().setAttribute(call.getScriptName(), object);
}
// Creator.PAGE scope means we create one every time anyway
}
}
// Some debug
if (log.isDebugEnabled())
{
StringBuffer buffer = new StringBuffer();
buffer.append("Exec: ")
.append(call.getScriptName())
.append(".")
.append(call.getMethodName())
.append("()");
if (create)
{
buffer.append(" Object created, ");
if (!scope.equals(Creator.PAGE))
{
buffer.append(" stored in ");
buffer.append(scope);
}
else
{
buffer.append(" not stored");
}
}
else
{
buffer.append(" Object found in ");
buffer.append(scope);
}
buffer.append(". ");
buffer.append("id=");
buffer.append(call.getCallId());
log.debug(buffer.toString());
}
// Execute the filter chain method.toString()
final Iterator<AjaxFilter> it = filters.iterator();
AjaxFilterChain chain = new AjaxFilterChain()
{
public Object doFilter(Object obj, Method meth, Object[] params) throws Exception
{
if (it.hasNext())
{
AjaxFilter next = it.next();
return next.doFilter(obj, meth, params, this);
}
else
{
return meth.invoke(obj, params);
}
}
};
Object reply = chain.doFilter(object, method, call.getParameters());
return new Reply(call.getCallId(), reply);
}
catch (SecurityException ex)
{
if (!filtersIncludeLogging(filters))
{
log.warn("Security Exception: " + ex.getMessage());
}
// If we are in live mode, then we don't even say what went wrong
if (debug)
{
return new Reply(call.getCallId(), null, ex);
}
else
{
return new Reply(call.getCallId(), null, new SecurityException());
}
}
catch (InvocationTargetException ex)
{
// Allow Jetty RequestRetry exception to propagate to container
Continuation.rethrowIfContinuation(ex);
debugException(filters, ex.getTargetException());
return new Reply(call.getCallId(), null, ex.getTargetException());
}
catch (Exception ex)
{
// Allow Jetty RequestRetry exception to propagate to container
Continuation.rethrowIfContinuation(ex);
debugException(filters, ex);
return new Reply(call.getCallId(), null, ex);
}
}
/**
* Do logging output if there are no logging filters and add a note of
* explanation the first time
* @param filters The configured filters
* @param ex The exception saying what broke
*/
private void debugException(List<AjaxFilter> filters, Throwable ex)
{
if (debug && !filtersIncludeLogging(filters))
{
if (!givenAuditLogHint)
{
log.debug("No logging filters defined. Minimal execption logging. For more detail add <filter class='org.directwebremoting.filter.AuditLogAjaxFilter'/> to dwr.xml");
givenAuditLogHint = true;
}
log.debug("Method execution failed: ", ex);
}
}
/**
* A quick check to see if we are already doing some form of logging
* @param filters The list of configured filters
* @return true if we are logging
*/
private boolean filtersIncludeLogging(List<AjaxFilter> filters)
{
for (AjaxFilter element : filters)
{
if (element instanceof LogAjaxFilter)
{
return true;
}
}
return false;
}
/**
* By default we use a relative path to the DWR servlet which can help if
* there are several routes to the servlet. However it can be a pain if
* the DWR engine is running on a different port from the web-server.
* However this is a minority case so this is not officially supported.
* @param useAbsolutePath Does DWR generate an absolute _path property
*/
public void setUseAbsolutePath(boolean useAbsolutePath)
{
this.useAbsolutePath = useAbsolutePath;
}
/**
* Accessor for the CreatorManager that we configure
* @param creatorManager The new ConverterManager
*/
public void setCreatorManager(CreatorManager creatorManager)
{
this.creatorManager = creatorManager;
}
/**
* Accessor for the ConverterManager that we configure
* @param converterManager The new ConverterManager
*/
public void setConverterManager(ConverterManager converterManager)
{
this.converterManager = converterManager;
}
/**
* Accessor for the security manager
* @param accessControl The accessControl to set.
*/
public void setAccessControl(AccessControl accessControl)
{
this.accessControl = accessControl;
}
/**
* Accessor for the AjaxFilterManager
* @param ajaxFilterManager The AjaxFilterManager to set.
*/
public void setAjaxFilterManager(AjaxFilterManager ajaxFilterManager)
{
this.ajaxFilterManager = ajaxFilterManager;
}
/**
* If we need to override the default path
* @param overridePath The new override path
*/
public void setOverridePath(String overridePath)
{
this.overridePath = overridePath;
}
/**
* Do we allow impossible tests for debug purposes
* @param allowImpossibleTests The allowImpossibleTests to set.
*/
public void setAllowImpossibleTests(boolean allowImpossibleTests)
{
this.allowImpossibleTests = allowImpossibleTests;
}
/**
* To prevent a DoS attack we limit the max number of calls that can be
* made in a batch
* @param maxCallCount the maxCallCount to set
*/
public void setMaxCallCount(int maxCallCount)
{
this.maxCallCount = maxCallCount;
}
/**
* Set the debug status
* @param debug The new debug setting
*/
public void setDebug(boolean debug)
{
this.debug = debug;
}
/**
* Have we given the hint about {@link org.directwebremoting.filter.AuditLogAjaxFilter}
*/
protected boolean givenAuditLogHint = false;
/**
* Are we in debug-mode and therefore more helpful at the expense of security?
*/
private boolean debug = false;
/**
* What AjaxFilters apply to which Ajax calls?
*/
private AjaxFilterManager ajaxFilterManager = null;
/**
* How we create new beans
*/
protected CreatorManager creatorManager = null;
/**
* How we convert beans - or in this case create client side classes
*/
protected ConverterManager converterManager = null;
/**
* The security manager
*/
protected AccessControl accessControl = null;
/**
* If we need to override the default path
*/
protected String overridePath = null;
/**
* @see #setUseAbsolutePath(boolean)
*/
protected boolean useAbsolutePath = false;
/**
* This helps us test that access rules are being followed
*/
protected boolean allowImpossibleTests = false;
/**
* To prevent a DoS attack we limit the max number of calls that can be
* made in a batch
*/
protected int maxCallCount = 20;
/**
* Generated Javascript cache
*/
protected Map<String, String> methodCache = Collections.synchronizedMap(new HashMap<String, String>());
/**
* The log stream
*/
private static final Log log = LogFactory.getLog(DefaultRemoter.class);
}