blob: 27a29573b0937177948303cc45e4007d0aef44f3 [file] [log] [blame]
/*
* $Id$
*
* 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.struts.dispatcher;
import org.apache.struts.chain.contexts.ActionContext;
import org.apache.struts.config.ActionConfig;
import org.apache.struts.util.MessageResources;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* TODO
*
* @version $Rev$
* @since Struts 1.4
*/
public abstract class AbstractDispatcher implements Dispatcher, Serializable {
// Package message bundle keys
static final String LOCAL_STRINGS = "org.apache.struts.dispatcher.LocalStrings";
static final String MSG_KEY_DISPATCH_ERROR = "dispatcher.error";
static final String MSG_KEY_MISSING_METHOD = "dispatcher.missingMethod";
static final String MSG_KEY_MISSING_METHOD_LOG = "dispatcher.missingMethod.log";
static final String MSG_KEY_MISSING_MAPPING_PARAMETER = "dispatcher.missingMappingParameter";
static final String MSG_KEY_UNSPECIFIED = "dispatcher.unspecified";
/**
* The name of the <code>cancelled</code> method.
*
* @see ActionContext#getCancelled()
*/
public static final String CANCELLED_METHOD_NAME = "cancelled";
/**
* The name of the <code>unspecified</code> method.
*
* @see #unspecified(ActionContext)
*/
public static final String UNSPECIFIED_METHOD_NAME = "unspecified";
/**
* The message resources for this package.
*/
static MessageResources messages = MessageResources.getMessageResources(LOCAL_STRINGS);
/**
* Shared commons Logging instance among subclasses.
*/
protected transient final Log log;
/**
* The dictionary of {@link Method} objects we have introspected for this
* class, keyed by method name. This collection is populated as different
* methods are called, so that introspection needs to occur only once per
* method name.
*/
private transient final HashMap methods;
/**
* Construct a new dispatcher.
*/
public AbstractDispatcher() {
log = LogFactory.getLog(getClass());
methods = new HashMap();
}
public Object dispatch(ActionContext context) throws Exception {
// Resolve the method name; fallback to default if necessary
String methodName = resolveMethodName(context);
if ((methodName == null) || "".equals(methodName)) {
methodName = getDefaultMethodName();
}
// Ensure there is a specified method name to invoke.
// This may be null if the user hacks the query string.
if (methodName == null) {
return unspecified(context);
}
// Identify the method object to dispatch
Method method;
try {
method = getMethod(context, methodName);
} catch (NoSuchMethodException e) {
// The log message reveals the offending method name...
String path = context.getActionConfig().getPath();
String message = messages.getMessage(MSG_KEY_MISSING_METHOD_LOG, path, methodName);
log.error(message, e);
// ...but the exception thrown does not
// See r383718 (XSS vulnerability)
String userMsg = messages.getMessage(MSG_KEY_MISSING_METHOD, path);
NoSuchMethodException e2 = new NoSuchMethodException(userMsg);
e2.initCause(e);
throw e2;
}
// Invoke the named method and return its result
return dispatchMethod(context, method, methodName);
}
/**
* Dispatch to the specified method.
*
* @param context the current action context
* @param method The method to invoke
* @param name The name of the method to invoke
* @return the return value of the method
* @throws Exception if the dispatch fails with an exception
*/
protected abstract Object dispatchMethod(ActionContext context, Method method, String name) throws Exception;
/**
* Empties the method cache.
*
* @see #getMethod(ActionContext, String)
*/
final void flushMethodCache() {
synchronized (methods) {
methods.clear();
}
}
/**
* Retrieves the name of the method to fallback upon if no method name can
* be resolved. The default implementation returns
* {@value #UNSPECIFIED_METHOD_NAME}.
*
* @return the fallback method name; can be <code>null</code>
* @see #resolveMethodName(ActionContext)
* @see #UNSPECIFIED_METHOD_NAME
*/
protected String getDefaultMethodName() {
return UNSPECIFIED_METHOD_NAME;
}
/**
* Introspects the action to identify a method of the specified name that
* will be the target of the dispatch. This implementation caches the method
* instance for subsequent invocations.
*
* @param context the current action context
* @param methodName the name of the method to be introspected
* @return the method of the specified name
* @throws NoSuchMethodException if no such method can be found
* @see #resolveMethod(ActionContext, String)
* @see #flushMethodCache()
*/
protected final Method getMethod(ActionContext context, String methodName) throws NoSuchMethodException {
synchronized (methods) {
// Key the method based on the class-method combination
StringBuffer keyBuf = new StringBuffer(100);
keyBuf.append(context.getAction().getClass().getName());
keyBuf.append(":");
keyBuf.append(methodName);
String key = keyBuf.toString();
Method method = (Method) methods.get(key);
if (method == null) {
method = resolveMethod(context, methodName);
methods.put(key, method);
}
return method;
}
}
/**
* Convenience method to help dispatch the specified method. The method is
* invoked via reflection.
*
* @param target the target object
* @param method the method of the target object
* @param args the arguments for the method
* @param path the mapping path
* @return the return value of the method
* @throws Exception if the dispatch fails with an exception
*/
protected final Object invoke(Object target, Method method, Object[] args, String path) throws Exception {
try {
return method.invoke(target, args);
} catch (IllegalAccessException e) {
String message = messages.getMessage(MSG_KEY_DISPATCH_ERROR, path);
log.error(message + ":" + method.getName(), e);
throw e;
} catch (InvocationTargetException e) {
// Rethrow the target exception if possible so that the
// exception handling machinery can deal with it
Throwable t = e.getTargetException();
if (t instanceof Exception) {
throw (Exception) t;
} else {
String message = messages.getMessage(MSG_KEY_DISPATCH_ERROR, path);
log.error(message + ":" + method.getName(), e);
throw new Exception(t);
}
}
}
/**
* Determines whether the current form's cancel button was pressed. The
* default behavior method will check if the
* {@link ActionContext#getCancelled()} context property is set , which
* normally occurs if the cancel button generated by <strong>CancelTag</strong>
* was pressed by the user in the current request.
*
* @param context the current action context
* @return <code>true</code> if the request is cancelled; otherwise
* <code>false</code>
* @see org.apache.struts.taglib.html.CancelTag
*/
protected boolean isCancelled(ActionContext context) {
Boolean cancelled = context.getCancelled();
return (cancelled != null) && cancelled.booleanValue();
}
/**
* Decides the appropriate method instance for the specified method name.
* Implementations may introspect for any desired method signature. This
* resolution is only invoked if {@link #getMethod(ActionContext, String)} does not find a
* match in its method cache.
*
* @param context the current action context
* @param methodName the method name to use for introspection
* @return the method to invoke
* @throws NoSuchMethodException if an appropriate method cannot be found
* @see #getMethod(ActionContext, String)
* @see #invoke(Object, Method, Object[], String)
*/
protected abstract Method resolveMethod(ActionContext context, String methodName) throws NoSuchMethodException;
/**
* Decides the method name that can handle the request.
*
* @param context the current action context
* @return the method name or <code>null</code> if no name can be resolved
* @see #getDefaultMethodName()
* @see #resolveMethod(ActionContext, String)
*/
protected abstract String resolveMethodName(ActionContext context);
/**
* Services the case when the dispatch fails because the method name cannot
* be resolved. The default behavior throws an {@link IllegalStateException}.
* Subclasses should override this to provide custom handling such as
* sending an HTTP 404 error or dispatching elsewhere.
*
* @param context the current action context
* @return the return value of the dispatch
* @throws Exception if an error occurs
* @see #resolveMethodName(ActionContext)
*/
protected Object unspecified(ActionContext context) throws Exception {
ActionConfig config = context.getActionConfig();
String msg = messages.getMessage(MSG_KEY_UNSPECIFIED, config.getPath());
log.error(msg);
throw new IllegalStateException(msg);
}
}