Merge pull request #621 from apache/WW-3737-custom-separator

[WW-3737] Allows to define a custom separator used to split patterns
This commit is contained in:
Lukasz Lenart
2022-10-21 09:23:52 +02:00
committed by GitHub
5 changed files with 202 additions and 130 deletions
@@ -34,22 +34,22 @@ import java.util.Locale;
import java.util.Map;
/**
* Base JUnit TestCase to extend for XWork specific JUnit tests. Uses
* Base JUnit TestCase to extend for XWork specific JUnit tests. Uses
* the generic test setup for logic.
*
* @author plightbo
*/
public abstract class XWorkTestCase extends TestCase {
protected ConfigurationManager configurationManager;
protected Configuration configuration;
protected Container container;
protected ActionProxyFactory actionProxyFactory;
public XWorkTestCase() {
super();
}
@Override
protected void setUp() throws Exception {
configurationManager = XWorkTestCaseHelper.setUp();
@@ -57,7 +57,7 @@ public abstract class XWorkTestCase extends TestCase {
container = configuration.getContainer();
actionProxyFactory = container.getInstance(ActionProxyFactory.class);
}
@Override
protected void tearDown() throws Exception {
XWorkTestCaseHelper.tearDown(configurationManager);
@@ -66,34 +66,33 @@ public abstract class XWorkTestCase extends TestCase {
container = null;
actionProxyFactory = null;
}
protected void loadConfigurationProviders(ConfigurationProvider... providers) {
configurationManager = XWorkTestCaseHelper.loadConfigurationProviders(configurationManager, providers);
configuration = configurationManager.getConfiguration();
container = configuration.getContainer();
actionProxyFactory = container.getInstance(ActionProxyFactory.class);
}
protected void loadButAdd(final Class<?> type, final Object impl) {
protected <T> void loadButAdd(final Class<T> type, final T impl) {
loadButAdd(type, Container.DEFAULT_NAME, impl);
}
protected void loadButAdd(final Class<?> type, final String name, final Object impl) {
protected <T> void loadButAdd(final Class<T> type, final String name, final T impl) {
loadConfigurationProviders(new StubConfigurationProvider() {
@Override
public void register(ContainerBuilder builder,
LocatableProperties props) throws ConfigurationException {
public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
if (impl instanceof String || ClassUtils.isPrimitiveOrWrapper(impl.getClass())) {
props.setProperty(name, "" + impl);
} else {
builder.factory(type, name, new Factory() {
public Object create(Context context) throws Exception {
builder.factory(type, name, new Factory<T>() {
public T create(Context context) throws Exception {
return impl;
}
@Override
public Class type() {
return impl.getClass();
public Class<T> type() {
return (Class<T>) impl.getClass();
}
}, Scope.SINGLETON);
}
@@ -19,10 +19,26 @@ import com.opensymphony.xwork2.inject.util.ReferenceCache;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ReflectPermission;
import java.security.AccessControlException;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Default {@link Container} implementation.
@@ -39,11 +55,7 @@ class ContainerImpl implements Container {
this.factories = factories;
final Map<Class<?>, Set<String>> map = new HashMap<>();
for (Key<?> key : factories.keySet()) {
Set<String> names = map.get(key.getType());
if (names == null) {
names = new HashSet<>();
map.put(key.getType(), names);
}
Set<String> names = map.computeIfAbsent(key.getType(), k -> new HashSet<>());
names.add(key.getName());
}
@@ -63,20 +75,20 @@ class ContainerImpl implements Container {
* Field and method injectors.
*/
final Map<Class<?>, List<Injector>> injectors =
new ReferenceCache<Class<?>, List<Injector>>() {
@Override
protected List<Injector> create(Class<?> key) {
List<Injector> injectors = new ArrayList<>();
addInjectors(key, injectors);
return injectors;
}
};
new ReferenceCache<Class<?>, List<Injector>>() {
@Override
protected List<Injector> create(Class<?> key) {
List<Injector> injectors = new ArrayList<>();
addInjectors(key, injectors);
return injectors;
}
};
/**
* Recursively adds injectors for fields and methods from the given class to the given list. Injects parent classes
* before sub classes.
*/
void addInjectors(Class clazz, List<Injector> injectors) {
void addInjectors(Class<?> clazz, List<Injector> injectors) {
if (clazz == Object.class) {
return;
}
@@ -97,38 +109,24 @@ class ContainerImpl implements Container {
addInjectorsForMethods(clazz.getDeclaredMethods(), true, injectors);
}
callInContext(new ContextualCallable<Void>() {
public Void call(InternalContext context) {
for (Injector injector : injectors) {
injector.inject(context, null);
}
return null;
callInContext((ContextualCallable<Void>) context -> {
for (Injector injector : injectors) {
injector.inject(context, null);
}
return null;
});
}
void addInjectorsForMethods(Method[] methods, boolean statics, List<Injector> injectors) {
addInjectorsForMembers(Arrays.asList(methods), statics, injectors,
new InjectorFactory<Method>() {
public Injector create(ContainerImpl container, Method method,
String name) throws MissingDependencyException {
return new MethodInjector(container, method, name);
}
});
addInjectorsForMembers(Arrays.asList(methods), statics, injectors, MethodInjector::new);
}
void addInjectorsForFields(Field[] fields, boolean statics, List<Injector> injectors) {
addInjectorsForMembers(Arrays.asList(fields), statics, injectors,
new InjectorFactory<Field>() {
public Injector create(ContainerImpl container, Field field,
String name) throws MissingDependencyException {
return new FieldInjector(container, field, name);
}
});
addInjectorsForMembers(Arrays.asList(fields), statics, injectors, FieldInjector::new);
}
<M extends Member & AnnotatedElement> void addInjectorsForMembers(
List<M> members, boolean statics, List<Injector> injectors, InjectorFactory<M> injectorFactory) {
List<M> members, boolean statics, List<Injector> injectors, InjectorFactory<M> injectorFactory) {
for (M member : members) {
if (isStatic(member) == statics) {
Inject inject = member.getAnnotation(Inject.class);
@@ -148,12 +146,12 @@ class ContainerImpl implements Container {
interface InjectorFactory<M extends Member & AnnotatedElement> {
Injector create(ContainerImpl container, M member, String name)
throws MissingDependencyException;
throws MissingDependencyException;
}
/**
* Determines if a given {@link Member} is static or not.
*
*
* @param member checked for the static modifier.
* @return true if member is static, false otherwise.
*/
@@ -163,13 +161,13 @@ class ContainerImpl implements Container {
/**
* Determines if a given {@link Member} is considered to be public for reflection usage or not.
*
*
* @param member checked to see if it is public for reflection usage.
* @return true if member is public for reflection usage, false otherwise.
*/
private static boolean isPublicForReflection(Member member) {
return Modifier.isPublic(member.getModifiers()) &&
Modifier.isPublic(member.getDeclaringClass().getModifiers());
Modifier.isPublic(member.getDeclaringClass().getModifiers());
}
static class FieldInjector implements Injector {
@@ -179,7 +177,7 @@ class ContainerImpl implements Container {
final ExternalContext<?> externalContext;
public FieldInjector(ContainerImpl container, Field field, String name)
throws MissingDependencyException {
throws MissingDependencyException {
this.field = field;
if (!isPublicForReflection(field) && !field.isAccessible()) {
SecurityManager sm = System.getSecurityManager();
@@ -190,7 +188,7 @@ class ContainerImpl implements Container {
field.setAccessible(true);
} catch (AccessControlException e) {
throw new DependencyException("Security manager in use, could not access field: "
+ field.getDeclaringClass().getName() + "(" + field.getName() + ")", e);
+ field.getDeclaringClass().getName() + "(" + field.getName() + ")", e);
}
}
@@ -225,8 +223,12 @@ class ContainerImpl implements Container {
* @param parameterTypes parameter types
* @return injections
*/
<M extends AccessibleObject & Member> ParameterInjector<?>[]
getParametersInjectors(M member, Annotation[][] annotations, Class[] parameterTypes, String defaultName) throws MissingDependencyException {
<M extends AccessibleObject & Member> ParameterInjector<?>[] getParametersInjectors(
M member,
Annotation[][] annotations,
Class<?>[] parameterTypes,
String defaultName
) throws MissingDependencyException {
final List<ParameterInjector<?>> parameterInjectors = new ArrayList<>();
final Iterator<Annotation[]> annotationsIterator = Arrays.asList(annotations).iterator();
@@ -247,12 +249,11 @@ class ContainerImpl implements Container {
}
final ExternalContext<T> externalContext = ExternalContext.newInstance(member, key, this);
return new ParameterInjector<T>(externalContext, factory);
return new ParameterInjector<>(externalContext, factory);
}
@SuppressWarnings("unchecked")
private ParameterInjector<?>[] toArray(List<ParameterInjector<?>> parameterInjections) {
return parameterInjections.toArray(new ParameterInjector[parameterInjections.size()]);
return parameterInjections.toArray(new ParameterInjector[0]);
}
/**
@@ -261,7 +262,7 @@ class ContainerImpl implements Container {
Inject findInject(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType() == Inject.class) {
return Inject.class.cast(annotation);
return (Inject) annotation;
}
}
return null;
@@ -283,7 +284,7 @@ class ContainerImpl implements Container {
method.setAccessible(true);
} catch (AccessControlException e) {
throw new DependencyException("Security manager in use, could not access method: "
+ name + "(" + method.getName() + ")", e);
+ name + "(" + method.getName() + ")", e);
}
}
@@ -292,7 +293,7 @@ class ContainerImpl implements Container {
throw new DependencyException(method + " has no parameters to inject.");
}
parameterInjectors = container.getParametersInjectors(
method, method.getParameterAnnotations(), parameterTypes, name);
method, method.getParameterAnnotations(), parameterTypes, name);
}
@Override
@@ -305,14 +306,12 @@ class ContainerImpl implements Container {
}
}
Map<Class<?>, ConstructorInjector> constructors =
new ReferenceCache<Class<?>, ConstructorInjector>() {
@Override
@SuppressWarnings("unchecked")
protected ConstructorInjector<?> create(Class<?> implementation) {
return new ConstructorInjector(ContainerImpl.this, implementation);
}
};
Map<Class<?>, ConstructorInjector<?>> constructors = new ReferenceCache<Class<?>, ConstructorInjector<?>>() {
@Override
protected ConstructorInjector<?> create(Class<?> implementation) {
return new ConstructorInjector<>(ContainerImpl.this, implementation);
}
};
static class ConstructorInjector<T> {
@@ -334,7 +333,7 @@ class ContainerImpl implements Container {
constructor.setAccessible(true);
} catch (AccessControlException e) {
throw new DependencyException("Security manager in use, could not access constructor: "
+ implementation.getName() + "(" + constructor.getName() + ")", e);
+ implementation.getName() + "(" + constructor.getName() + ")", e);
}
}
@@ -359,14 +358,14 @@ class ContainerImpl implements Container {
}
ParameterInjector<?>[] constructParameterInjector(
Inject inject, ContainerImpl container, Constructor<T> constructor) throws MissingDependencyException {
Inject inject, ContainerImpl container, Constructor<T> constructor) throws MissingDependencyException {
return constructor.getParameterTypes().length == 0
? null // default constructor.
: container.getParametersInjectors(
constructor,
constructor.getParameterAnnotations(),
constructor.getParameterTypes(),
inject.value()
? null // default constructor.
: container.getParametersInjectors(
constructor,
constructor.getParameterAnnotations(),
constructor.getParameterTypes(),
inject.value()
);
}
@@ -378,7 +377,7 @@ class ContainerImpl implements Container {
if (constructor.getAnnotation(Inject.class) != null) {
if (found != null) {
throw new DependencyException("More than one constructor annotated"
+ " with @Inject found in " + implementation + ".");
+ " with @Inject found in " + implementation + ".");
}
found = constructor;
}
@@ -466,7 +465,7 @@ class ContainerImpl implements Container {
}
}
private static Object[] getParameters(Member member, InternalContext context, ParameterInjector[] parameterInjectors) {
private static Object[] getParameters(Member member, InternalContext context, ParameterInjector<?>[] parameterInjectors) {
if (parameterInjectors == null) {
return null;
}
@@ -494,13 +493,12 @@ class ContainerImpl implements Container {
}
}
@SuppressWarnings("unchecked")
<T> T getInstance(Class<T> type, String name, InternalContext context) {
final ExternalContext<?> previous = context.getExternalContext();
final Key<T> key = Key.newInstance(type, name);
context.setExternalContext(ExternalContext.newInstance(null, key, this));
try {
final InternalFactory o = getFactory(key);
final InternalFactory<? extends T> o = getFactory(key);
if (o != null) {
return getFactory(key).create(context);
} else {
@@ -517,39 +515,25 @@ class ContainerImpl implements Container {
@Override
public void inject(final Object o) {
callInContext(new ContextualCallable<Void>() {
public Void call(InternalContext context) {
inject(o, context);
return null;
}
callInContext((ContextualCallable<Void>) context -> {
inject(o, context);
return null;
});
}
@Override
public <T> T inject(final Class<T> implementation) {
return callInContext(new ContextualCallable<T>() {
public T call(InternalContext context) {
return inject(implementation, context);
}
});
return callInContext(context -> inject(implementation, context));
}
@Override
public <T> T getInstance(final Class<T> type, final String name) {
return callInContext(new ContextualCallable<T>() {
public T call(InternalContext context) {
return getInstance(type, name, context);
}
});
return callInContext(context -> getInstance(type, name, context));
}
@Override
public <T> T getInstance(final Class<T> type) {
return callInContext(new ContextualCallable<T>() {
public T call(InternalContext context) {
return getInstance(type, context);
}
});
return callInContext(context -> getInstance(type, context));
}
@Override
@@ -561,12 +545,7 @@ class ContainerImpl implements Container {
return names;
}
ThreadLocal<Object[]> localContext = new ThreadLocal<Object[]>() {
@Override
protected Object[] initialValue() {
return new Object[1];
}
};
ThreadLocal<Object[]> localContext = ThreadLocal.withInitial(() -> new Object[1]);
/**
* Looks up thread local context. Creates (and removes) a new context if necessary.
@@ -598,7 +577,7 @@ class ContainerImpl implements Container {
*/
@SuppressWarnings("unchecked")
<T> ConstructorInjector<T> getConstructor(Class<T> implementation) {
return constructors.get(implementation);
return (ConstructorInjector<T>) constructors.get(implementation);
}
final ThreadLocal<Object> localScopeStrategy = new ThreadLocal<>();
@@ -56,6 +56,9 @@ public final class StrutsConstants {
/** Comma separated list of patterns (java.util.regex.Pattern) to be excluded from Struts2-processing */
public static final String STRUTS_ACTION_EXCLUDE_PATTERN = "struts.action.excludePattern";
/** A custom separator used to split list of patterns (java.util.regex.Pattern) to be excluded from Struts2-processing */
public static final String STRUTS_ACTION_EXCLUDE_PATTERN_SEPARATOR = "struts.action.excludePattern.separator";
/** Whether to use the response encoding (JSP page encoding) for s:include tag processing (false - use STRUTS_I18N_ENCODING - by default) */
public static final String STRUTS_TAG_INCLUDETAG_USERESPONSEENCODING = "struts.tag.includetag.useResponseEncoding";
@@ -21,7 +21,12 @@ package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.ActionContext;
import org.apache.struts2.StrutsConstants;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
@@ -36,10 +41,9 @@ public class InitOperations {
* Creates and initializes the dispatcher
*
* @param filterConfig host configuration
*
* @return the dispatcher
*/
public Dispatcher initDispatcher( HostConfig filterConfig ) {
public Dispatcher initDispatcher(HostConfig filterConfig) {
Dispatcher dispatcher = createDispatcher(filterConfig);
dispatcher.init();
return dispatcher;
@@ -49,10 +53,10 @@ public class InitOperations {
* Initializes the static content loader with the filter configuration
*
* @param filterConfig host configuration
* @param dispatcher the dispatcher
* @param dispatcher the dispatcher
* @return the static content loader
*/
public StaticContentLoader initStaticContentLoader( HostConfig filterConfig, Dispatcher dispatcher ) {
public StaticContentLoader initStaticContentLoader(HostConfig filterConfig, Dispatcher dispatcher) {
StaticContentLoader loader = dispatcher.getContainer().getInstance(StaticContentLoader.class);
loader.setHostConfig(filterConfig);
return loader;
@@ -60,7 +64,6 @@ public class InitOperations {
/**
* @return The dispatcher on the thread.
*
* @throws IllegalStateException If there is no dispatcher available
*/
public Dispatcher findDispatcherOnThread() {
@@ -75,12 +78,11 @@ public class InitOperations {
* Create a {@link Dispatcher}
*
* @param filterConfig host configuration
*
* @return The dispatcher on the thread.
*/
protected Dispatcher createDispatcher(HostConfig filterConfig) {
Map<String, String> params = new HashMap<>();
for ( Iterator<String> parameterNames = filterConfig.getInitParameterNames(); parameterNames.hasNext(); ) {
for (Iterator<String> parameterNames = filterConfig.getInitParameterNames(); parameterNames.hasNext(); ) {
String name = parameterNames.next();
String value = filterConfig.getInitParameter(name);
params.put(name, value);
@@ -96,20 +98,23 @@ public class InitOperations {
* Extract a list of patterns to exclude from request filtering
*
* @param dispatcher The dispatcher to check for exclude pattern configuration
*
* @return a List of Patterns for request to exclude if apply, or <tt>null</tt>
*
* @see org.apache.struts2.StrutsConstants#STRUTS_ACTION_EXCLUDE_PATTERN
*/
public List<Pattern> buildExcludedPatternsList( Dispatcher dispatcher ) {
return buildExcludedPatternsList(dispatcher.getContainer().getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN));
public List<Pattern> buildExcludedPatternsList(Dispatcher dispatcher) {
String excludePatterns = dispatcher.getContainer().getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN);
String separator = dispatcher.getContainer().getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN_SEPARATOR);
if (separator == null) {
separator = ",";
}
return buildExcludedPatternsList(excludePatterns, separator);
}
private List<Pattern> buildExcludedPatternsList( String patterns ) {
private List<Pattern> buildExcludedPatternsList(String patterns, String separator) {
if (null != patterns && patterns.trim().length() != 0) {
List<Pattern> list = new ArrayList<>();
String[] tokens = patterns.split(",");
for ( String token : tokens ) {
String[] tokens = patterns.split(separator);
for (String token : tokens) {
list.add(Pattern.compile(token.trim()));
}
return Collections.unmodifiableList(list);
@@ -0,0 +1,86 @@
/*
* 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.dispatcher;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.inject.ContainerBuilder;
import com.opensymphony.xwork2.util.location.LocatableProperties;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.config.PropertiesConfigurationProvider;
import java.util.List;
import java.util.regex.Pattern;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class InitOperationsTest extends StrutsInternalTestCase {
public void testExcludePatterns() {
// given
loadConfigurationProviders(new PropertiesConfigurationProvider() {
@Override
public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
props.setProperty(StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN, "/ns1/.*\\.json,/ns2/.*\\.json");
}
});
Dispatcher mockDispatcher = mock(Dispatcher.class);
when(mockDispatcher.getContainer()).thenReturn(container);
// when
InitOperations init = new InitOperations();
List<Pattern> patterns = init.buildExcludedPatternsList(mockDispatcher);
// then
assertThat(patterns).extracting(Pattern::toString).containsOnly(
"/ns1/.*\\.json",
"/ns2/.*\\.json"
);
}
public void testExcludePatternsUsingCustomSeparator() {
// given
loadConfigurationProviders(new PropertiesConfigurationProvider() {
@Override
public void register(ContainerBuilder builder, LocatableProperties props) throws ConfigurationException {
props.setProperty(StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN, "/ns1/[a-z]{1,10}.json///ns2/[a-z]{1,10}.json");
props.setProperty(StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN_SEPARATOR, "//");
}
});
Dispatcher mockDispatcher = mock(Dispatcher.class);
when(mockDispatcher.getContainer()).thenReturn(container);
// when
InitOperations init = new InitOperations();
String separator = container.getInstance(String.class, StrutsConstants.STRUTS_ACTION_EXCLUDE_PATTERN_SEPARATOR);
List<Pattern> patterns = init.buildExcludedPatternsList(mockDispatcher);
// then
assertThat(separator).isNotBlank().isEqualTo("//");
assertThat(patterns).extracting(Pattern::toString).containsOnly(
"/ns1/[a-z]{1,10}.json",
"/ns2/[a-z]{1,10}.json"
);
}
}