org.apache.struts2.convention.ConventionUnknownHandler Maven / Gradle / Ivy
The newest version!
/*
* 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.struts2.convention;
import com.opensymphony.xwork2.*;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.entities.*;
import com.opensymphony.xwork2.config.providers.InterceptorBuilder;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import com.opensymphony.xwork2.util.TextParseUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsException;
import jakarta.servlet.ServletContext;
import java.net.MalformedURLException;
import java.util.*;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
*
* This class is the default unknown handler for all of the Convention
* plugin integration with XWork. This handles instances when a URL doesn't
* have an action associated with it but does a viable result and also the
* instance where an action returns a result code that isn't already
* configured but there is a viable result for the code.
*
*
*
* This class also handles all of the index actions using redirects
* and actions in nested packages. For example, if there is an action
* /foo/index and the URL /foo is used,
* this will render the index action in the /foo namespace.
*
*/
public class ConventionUnknownHandler implements UnknownHandler {
private static final Logger LOG = LogManager.getLogger(ConventionUnknownHandler.class);
protected Configuration configuration;
protected ObjectFactory objectFactory;
protected ServletContext servletContext;
protected ResultMapBuilder resultMapBuilder;
protected String defaultParentPackageName;
protected PackageConfig parentPackage;
private boolean redirectToSlash;
private ConventionsService conventionsService;
private String nameSeparator;
protected Set allowedMethods;
/**
* Constructs the unknown handler.
*
* @param configuration The XWork configuration.
* @param objectFactory The XWork object factory used to create result instances.
* @param servletContext The servlet context used to help build the action configurations.
* @param container The Xwork container
* @param defaultParentPackageName The default XWork package that the unknown handler will use as
* the parent package for new actions and results.
* @param redirectToSlash A boolean parameter that controls whether or not this will handle
* unknown actions in the same manner as Apache, Tomcat and other web servers. This
* handling will send back a redirect for URLs such as /foo to /foo/ if there doesn't
* exist an action that responds to /foo.
* @param nameSeparator The character used as word separator in the action names. "-" by default
*/
@Inject
public ConventionUnknownHandler(Configuration configuration, ObjectFactory objectFactory,
ServletContext servletContext, Container container,
@Inject(ConventionConstants.CONVENTION_DEFAULT_PARENT_PACKAGE) String defaultParentPackageName,
@Inject(ConventionConstants.CONVENTION_REDIRECT_TO_SLASH) String redirectToSlash,
@Inject(ConventionConstants.CONVENTION_ACTION_NAME_SEPARATOR) String nameSeparator) {
this.configuration = configuration;
this.objectFactory = objectFactory;
this.servletContext = servletContext;
this.resultMapBuilder = container.getInstance(ResultMapBuilder.class, container.getInstance(String.class, ConventionConstants.CONVENTION_RESULT_MAP_BUILDER));
this.conventionsService = container.getInstance(ConventionsService.class, container.getInstance(String.class, ConventionConstants.CONVENTION_CONVENTIONS_SERVICE));
this.defaultParentPackageName = defaultParentPackageName;
this.nameSeparator = nameSeparator;
this.parentPackage = configuration.getPackageConfig(defaultParentPackageName);
if (parentPackage == null) {
throw new ConfigurationException("Unknown default parent package [" + defaultParentPackageName + "]");
}
this.redirectToSlash = Boolean.parseBoolean(redirectToSlash);
allowedMethods = TextParseUtil.commaDelimitedStringToSet("execute,input,back,cancel,browse,index");
}
public ActionConfig handleUnknownAction(String namespace, String actionName)
throws StrutsException {
// Strip the namespace if it is just a slash
if (namespace == null || "/".equals(namespace)) {
namespace = "";
}
Map resultsByExtension = conventionsService.getResultTypesByExtension(parentPackage);
String pathPrefix = determinePath(null, namespace);
ActionConfig actionConfig = null;
// Try /idx/action.jsp if actionName is not empty, otherwise it will just be /.jsp
if (!actionName.equals("")) {
Resource resource = findResource(resultsByExtension, pathPrefix, actionName);
if (resource != null) {
actionConfig = buildActionConfig(resource.path, resultsByExtension.get(resource.ext));
}
}
if (actionConfig == null) {
Resource resource = findResource(resultsByExtension, pathPrefix, actionName, "/index");
// If the URL is /foo and there is an action we can redirect to, send the redirect to /foo/.
// However, if that action is not in the same namespace, it is the default, so I'm not going
// to return that.
if (!actionName.equals("") && redirectToSlash) {
ResultTypeConfig redirectResultTypeConfig = parentPackage.getAllResultTypeConfigs().get("redirect");
String redirectNamespace = namespace + "/" + actionName;
LOG.trace("Checking if there is an action named index in the namespace {}", redirectNamespace);
actionConfig = configuration.getRuntimeConfiguration().getActionConfig(redirectNamespace, "index");
if (actionConfig != null) {
LOG.trace("Found action config");
PackageConfig packageConfig = configuration.getPackageConfig(actionConfig.getPackageName());
if (redirectNamespace.equals(packageConfig.getNamespace())) {
LOG.trace("Action is not a default - redirecting");
return buildActionConfig(redirectNamespace + "/", redirectResultTypeConfig);
}
LOG.trace("Action was a default - NOT redirecting");
}
if (resource != null) {
return buildActionConfig(redirectNamespace + "/", redirectResultTypeConfig);
}
}
if (resource != null) {
// Otherwise, if the URL is /foo or /foo/ look for index pages in /foo/
actionConfig = buildActionConfig(resource.path, resultsByExtension.get(resource.ext));
}
// try to find index action in current namespace or in default one
if (actionConfig == null) {
LOG.trace("Looking for action named [index] in namespace [{}] or in default namespace", namespace);
actionConfig = configuration.getRuntimeConfiguration().getActionConfig(namespace, "index");
}
}
return actionConfig;
}
/**
* Finds a resource using the given path parts and all of the extensions in the map.
*
* @param resultsByExtension Map of extension to result type config objects.
* @param parts The parts of the resource.
* @return The resource path or null.
*/
protected Resource findResource(Map resultsByExtension, String... parts) {
for (String ext : resultsByExtension.keySet()) {
String canonicalPath = canonicalize(string(parts) + "." + ext);
LOG.trace("Checking for {}", canonicalPath);
try {
if (servletContext.getResource(canonicalPath) != null) {
return new Resource(canonicalPath, ext);
}
} catch (MalformedURLException e) {
LOG.error("Unable to parse path to the web application resource {} skipping...", canonicalPath);
}
}
return null;
}
protected String canonicalize(final String path) {
if (path == null) {
return null;
}
return path.replaceAll("/+", "/");
}
protected ActionConfig buildActionConfig(String path, ResultTypeConfig resultTypeConfig) {
Map results = new HashMap<>();
HashMap params = new HashMap<>();
if (resultTypeConfig.getParams() != null) {
params.putAll(resultTypeConfig.getParams());
}
params.put(resultTypeConfig.getDefaultResultParam(), path);
PackageConfig pkg = configuration.getPackageConfig(defaultParentPackageName);
List interceptors = InterceptorBuilder.constructInterceptorReference(pkg, pkg.getFullDefaultInterceptorRef(), Collections.emptyMap(), null, objectFactory);
ResultConfig config = new ResultConfig.Builder(Action.SUCCESS, resultTypeConfig.getClassName()).
addParams(params).build();
results.put(Action.SUCCESS, config);
return new ActionConfig.Builder(defaultParentPackageName, "execute", ActionSupport.class.getName()).
addInterceptors(interceptors).
addResultConfigs(results).
addAllowedMethod(pkg.getGlobalAllowedMethods()).
build();
}
private Result scanResultsByExtension(String ns, String actionName, String pathPrefix,
String resultCode, ActionContext actionContext) {
Map resultsByExtension = conventionsService.getResultTypesByExtension(parentPackage);
Result result = null;
for (String ext : resultsByExtension.keySet()) {
if (LOG.isTraceEnabled()) {
String fqan = ns + "/" + actionName;
LOG.trace("Trying to locate the correct result for the FQ action [{}]"
+ " with an file extension of [{}] in the directory [{}] " + "and a result code of [{}]",
fqan, ext, pathPrefix, resultCode);
}
String path = string(pathPrefix, actionName, nameSeparator, resultCode, ".", ext);
result = findResult(path, resultCode, ext, actionContext, resultsByExtension);
if (result != null) {
break;
}
path = string(pathPrefix, actionName, ".", ext);
result = findResult(path, resultCode, ext, actionContext, resultsByExtension);
if (result != null) {
break;
}
// Issue #6 - Scan for result-code as page name
path = string(pathPrefix, resultCode, ".", ext);
result = findResult(path, resultCode, ext, actionContext, resultsByExtension);
if (result != null) {
break;
}
}
return result;
}
public Result handleUnknownResult(ActionContext actionContext, String actionName,
ActionConfig actionConfig, String resultCode) throws StrutsException {
PackageConfig pkg = configuration.getPackageConfig(actionConfig.getPackageName());
String ns = pkg.getNamespace();
String pathPrefix = determinePath(actionConfig, ns);
Result result = scanResultsByExtension(ns, actionName, pathPrefix, resultCode, actionContext);
if (result == null) {
// Try /idx/action/index.jsp
Map resultsByExtension = conventionsService.getResultTypesByExtension(pkg);
for (String ext : resultsByExtension.keySet()) {
if (LOG.isTraceEnabled()) {
String fqan = ns + "/" + actionName;
LOG.trace("Checking for [{}/index.{}]", fqan, ext);
}
String path = string(pathPrefix, actionName, "/index", nameSeparator, resultCode, ".", ext);
result = findResult(path, resultCode, ext, actionContext, resultsByExtension);
if (result != null) {
break;
}
path = string(pathPrefix, actionName, "/index.", ext);
result = findResult(path, resultCode, ext, actionContext, resultsByExtension);
if (result != null) {
break;
}
}
}
if (result == null && resultCode != null) {
//try to find an action to chain to. If the source action is "foo" and
//the result is "bar", we will try to find an action called "foo-bar"
//in the same package
String chainedTo = actionName + nameSeparator + resultCode;
ActionConfig chainedToConfig = pkg.getActionConfigs().get(chainedTo);
if (chainedToConfig != null) {
LOG.trace("Action [{}] used as chain result for [{}] and result [{}]", chainedTo, actionName, resultCode);
ResultTypeConfig chainResultType = pkg.getAllResultTypeConfigs().get("chain");
result = buildResult(chainedTo, resultCode, chainResultType, actionContext);
}
}
return result;
}
protected Result findResult(String path, String resultCode, String ext, ActionContext actionContext,
Map resultsByExtension) {
try {
LOG.trace("Checking ServletContext for {}", path);
URL resource = servletContext.getResource(path);
if (resource != null && resource.getPath().endsWith(path)) {
LOG.trace("Found resource {}", resource);
return buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
}
LOG.trace("Checking ClassLoader for {}", path);
String classLoaderPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
resource = ClassLoaderUtil.getResource(classLoaderPath, getClass());
if (resource != null && resource.getPath().endsWith(classLoaderPath)) {
LOG.trace("Found resource {}", resource);
return buildResult(path, resultCode, resultsByExtension.get(ext), actionContext);
}
} catch (MalformedURLException e) {
LOG.error("Unable to parse template path: {} skipping...", path);
}
return null;
}
protected Result buildResult(String path, String resultCode, ResultTypeConfig config, ActionContext invocationContext) {
String resultClass = config.getClassName();
Map params = new LinkedHashMap<>();
if (config.getParams() != null) {
params.putAll(config.getParams());
}
params.put(config.getDefaultResultParam(), path);
ResultConfig resultConfig = new ResultConfig.Builder(resultCode, resultClass).addParams(params).build();
try {
return objectFactory.buildResult(resultConfig, invocationContext.getContextMap());
} catch (Exception e) {
throw new StrutsException("Unable to build convention result", e, resultConfig);
}
}
protected String string(String... parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part);
}
return sb.toString();
}
/**
* Determines the result path prefix that the request URL is for, minus the action name. This includes
* the base result location and the namespace, with all the slashes handled.
*
* @param actionConfig (Optional) The might be a ConventionActionConfig, from which we can get the
* default base result location of that specific action.
* @param namespace The current URL namespace.
* @return The path prefix and never null.
*/
protected String determinePath(ActionConfig actionConfig, String namespace) {
String finalPrefix = conventionsService.determineResultPath(actionConfig);
if (!finalPrefix.endsWith("/")) {
finalPrefix += "/";
}
if (StringUtils.equals(namespace,"/" )) {
namespace = "";
}
if (namespace.length() > 0) {
if (namespace.startsWith("/")) {
namespace = namespace.substring(1);
}
if (!namespace.endsWith("/")) {
namespace += "/";
}
}
return finalPrefix + namespace;
}
/**
* Not used
*/
public Object handleUnknownActionMethod(Object action, String methodName) {
return null;
}
public static class Resource {
final String path;
final String ext;
public Resource(String path, String ext) {
this.path = path;
this.ext = ext;
}
}
}