You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2015/06/17 23:09:41 UTC
[41/57] [partial] struts git commit: Merges xwork packages into struts
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceMap.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceMap.java b/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceMap.java
new file mode 100644
index 0000000..4d8eebb
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceMap.java
@@ -0,0 +1,605 @@
+/**
+ * Copyright (C) 2006 Google Inc.
+ * <p/>
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * 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 com.opensymphony.xwork2.inject.util;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.ref.Reference;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static com.opensymphony.xwork2.inject.util.ReferenceType.STRONG;
+
+/**
+ * Concurrent hash map that wraps keys and/or values in soft or weak
+ * references. Does not support null keys or values. Uses identity equality
+ * for weak and soft keys.
+ *
+ * <p>The concurrent semantics of {@link ConcurrentHashMap} combined with the
+ * fact that the garbage collector can asynchronously reclaim and clean up
+ * after keys and values at any time can lead to some racy semantics. For
+ * example, {@link #size()} returns an upper bound on the size, i.e. the actual
+ * size may be smaller in cases where the key or value has been reclaimed but
+ * the map entry has not been cleaned up yet.
+ *
+ * <p>Another example: If {@link #get(Object)} cannot find an existing entry
+ * for a key, it will try to create one. This operation is not atomic. One
+ * thread could {@link #put(Object, Object)} a value between the time another
+ * thread running {@code get()} checks for an entry and decides to create one.
+ * In this case, the newly created value will replace the put value in the
+ * map. Also, two threads running {@code get()} concurrently can potentially
+ * create duplicate values for a given key.
+ *
+ * <p>In other words, this class is great for caching but not atomicity.
+ *
+ * @author crazybob@google.com (Bob Lee)
+ */
+@SuppressWarnings("unchecked")
+public class ReferenceMap<K, V> implements Map<K, V>, Serializable {
+
+ private static final long serialVersionUID = 0;
+
+ transient ConcurrentMap<Object, Object> delegate;
+
+ final ReferenceType keyReferenceType;
+ final ReferenceType valueReferenceType;
+
+ /**
+ * Concurrent hash map that wraps keys and/or values based on specified
+ * reference types.
+ *
+ * @param keyReferenceType key reference type
+ * @param valueReferenceType value reference type
+ */
+ public ReferenceMap(ReferenceType keyReferenceType,
+ ReferenceType valueReferenceType) {
+ ensureNotNull(keyReferenceType, valueReferenceType);
+
+ if (keyReferenceType == ReferenceType.PHANTOM || valueReferenceType == ReferenceType.PHANTOM) {
+ throw new IllegalArgumentException("Phantom references not supported.");
+ }
+
+ this.delegate = new ConcurrentHashMap<>();
+ this.keyReferenceType = keyReferenceType;
+ this.valueReferenceType = valueReferenceType;
+ }
+
+ V internalGet(K key) {
+ Object valueReference = delegate.get(makeKeyReferenceAware(key));
+ return valueReference == null ? null : (V) dereferenceValue(valueReference);
+ }
+
+ public V get(final Object key) {
+ ensureNotNull(key);
+ return internalGet((K) key);
+ }
+
+ V execute(Strategy strategy, K key, V value) {
+ ensureNotNull(key, value);
+ Object keyReference = referenceKey(key);
+ Object valueReference = strategy.execute(this, keyReference, referenceValue(keyReference, value));
+ return valueReference == null ? null : (V) dereferenceValue(valueReference);
+ }
+
+ public V put(K key, V value) {
+ return execute(putStrategy(), key, value);
+ }
+
+ public V remove(Object key) {
+ ensureNotNull(key);
+ Object referenceAwareKey = makeKeyReferenceAware(key);
+ Object valueReference = delegate.remove(referenceAwareKey);
+ return valueReference == null ? null : (V) dereferenceValue(valueReference);
+ }
+
+ public int size() {
+ return delegate.size();
+ }
+
+ public boolean isEmpty() {
+ return delegate.isEmpty();
+ }
+
+ public boolean containsKey(Object key) {
+ ensureNotNull(key);
+ Object referenceAwareKey = makeKeyReferenceAware(key);
+ return delegate.containsKey(referenceAwareKey);
+ }
+
+ public boolean containsValue(Object value) {
+ ensureNotNull(value);
+ for (Object valueReference : delegate.values()) {
+ if (value.equals(dereferenceValue(valueReference))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void putAll(Map<? extends K, ? extends V> t) {
+ for (Map.Entry<? extends K, ? extends V> entry : t.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ public void clear() {
+ delegate.clear();
+ }
+
+ /**
+ * Returns an unmodifiable set view of the keys in this map. As this method
+ * creates a defensive copy, the performance is O(n).
+ */
+ public Set<K> keySet() {
+ return Collections.unmodifiableSet(dereferenceKeySet(delegate.keySet()));
+ }
+
+ /**
+ * Returns an unmodifiable set view of the values in this map. As this
+ * method creates a defensive copy, the performance is O(n).
+ */
+ public Collection<V> values() {
+ return Collections.unmodifiableCollection(dereferenceValues(delegate.values()));
+ }
+
+ public V putIfAbsent(K key, V value) {
+ // TODO (crazybob) if the value has been gc'ed but the entry hasn't been
+ // cleaned up yet, this put will fail.
+ return execute(putIfAbsentStrategy(), key, value);
+ }
+
+ public boolean remove(Object key, Object value) {
+ ensureNotNull(key, value);
+ Object referenceAwareKey = makeKeyReferenceAware(key);
+ Object referenceAwareValue = makeValueReferenceAware(value);
+ return delegate.remove(referenceAwareKey, referenceAwareValue);
+ }
+
+ public boolean replace(K key, V oldValue, V newValue) {
+ ensureNotNull(key, oldValue, newValue);
+ Object keyReference = referenceKey(key);
+
+ Object referenceAwareOldValue = makeValueReferenceAware(oldValue);
+ return delegate.replace(keyReference, referenceAwareOldValue, referenceValue(keyReference, newValue));
+ }
+
+ public V replace(K key, V value) {
+ // TODO (crazybob) if the value has been gc'ed but the entry hasn't been
+ // cleaned up yet, this will succeed when it probably shouldn't.
+ return execute(replaceStrategy(), key, value);
+ }
+
+ /**
+ * Returns an unmodifiable set view of the entries in this map. As this
+ * method creates a defensive copy, the performance is O(n).
+ */
+ public Set<Map.Entry<K, V>> entrySet() {
+ Set<Map.Entry<K, V>> entrySet = new HashSet<>();
+ for (Map.Entry<Object, Object> entry : delegate.entrySet()) {
+ Map.Entry<K, V> dereferenced = dereferenceEntry(entry);
+ if (dereferenced != null) {
+ entrySet.add(dereferenced);
+ }
+ }
+ return Collections.unmodifiableSet(entrySet);
+ }
+
+ /**
+ * Dereferences an entry. Returns null if the key or value has been gc'ed.
+ */
+ Entry dereferenceEntry(Map.Entry<Object, Object> entry) {
+ K key = dereferenceKey(entry.getKey());
+ V value = dereferenceValue(entry.getValue());
+ return (key == null || value == null) ? null : new Entry(key, value);
+ }
+
+ /**
+ * Creates a reference for a key.
+ */
+ Object referenceKey(K key) {
+ switch (keyReferenceType) {
+ case STRONG:
+ return key;
+ case SOFT:
+ return new SoftKeyReference(key);
+ case WEAK:
+ return new WeakKeyReference(key);
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Converts a reference to a key.
+ */
+ K dereferenceKey(Object o) {
+ return (K) dereference(keyReferenceType, o);
+ }
+
+ /**
+ * Converts a reference to a value.
+ */
+ V dereferenceValue(Object o) {
+ return (V) dereference(valueReferenceType, o);
+ }
+
+ /**
+ * Returns the refererent for reference given its reference type.
+ */
+ Object dereference(ReferenceType referenceType, Object reference) {
+ return referenceType == STRONG ? reference : ((Reference) reference).get();
+ }
+
+ /**
+ * Creates a reference for a value.
+ */
+ Object referenceValue(Object keyReference, Object value) {
+ switch (valueReferenceType) {
+ case STRONG:
+ return value;
+ case SOFT:
+ return new SoftValueReference(keyReference, value);
+ case WEAK:
+ return new WeakValueReference(keyReference, value);
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Dereferences a set of key references.
+ */
+ Set<K> dereferenceKeySet(Set keyReferences) {
+ return keyReferenceType == STRONG
+ ? keyReferences
+ : dereferenceCollection(keyReferenceType, keyReferences, new HashSet());
+ }
+
+ /**
+ * Dereferences a collection of value references.
+ */
+ Collection<V> dereferenceValues(Collection valueReferences) {
+ return valueReferenceType == STRONG
+ ? valueReferences
+ : dereferenceCollection(valueReferenceType, valueReferences,
+ new ArrayList(valueReferences.size()));
+ }
+
+ /**
+ * Wraps key so it can be compared to a referenced key for equality.
+ */
+ Object makeKeyReferenceAware(Object o) {
+ return keyReferenceType == STRONG ? o : new KeyReferenceAwareWrapper(o);
+ }
+
+ /**
+ * Wraps value so it can be compared to a referenced value for equality.
+ */
+ Object makeValueReferenceAware(Object o) {
+ return valueReferenceType == STRONG ? o : new ReferenceAwareWrapper(o);
+ }
+
+ /**
+ * Dereferences elements in {@code in} using
+ * {@code referenceType} and puts them in {@code out}. Returns
+ * {@code out}.
+ */
+ <T extends Collection<Object>> T dereferenceCollection(ReferenceType referenceType, T in, T out) {
+ for (Object reference : in) {
+ out.add(dereference(referenceType, reference));
+ }
+ return out;
+ }
+
+ /**
+ * Marker interface to differentiate external and internal references.
+ */
+ interface InternalReference {
+ }
+
+ static int keyHashCode(Object key) {
+ return System.identityHashCode(key);
+ }
+
+ /**
+ * Tests weak and soft references for identity equality. Compares references
+ * to other references and wrappers. If o is a reference, this returns true
+ * if r == o or if r and o reference the same non null object. If o is a
+ * wrapper, this returns true if r's referent is identical to the wrapped
+ * object.
+ */
+ static boolean referenceEquals(Reference r, Object o) {
+ // compare reference to reference.
+ if (o instanceof InternalReference) {
+ // are they the same reference? used in cleanup.
+ if (o == r) {
+ return true;
+ }
+
+ // do they reference identical values? used in conditional puts.
+ Object referent = ((Reference) o).get();
+ return referent != null && referent == r.get();
+ }
+
+ // is the wrapped object identical to the referent? used in lookups.
+ return ((ReferenceAwareWrapper) o).unwrap() == r.get();
+ }
+
+ /**
+ * Big hack. Used to compare keys and values to referenced keys and values
+ * without creating more references.
+ */
+ static class ReferenceAwareWrapper {
+
+ Object wrapped;
+
+ ReferenceAwareWrapper(Object wrapped) {
+ this.wrapped = wrapped;
+ }
+
+ Object unwrap() {
+ return wrapped;
+ }
+
+ @Override
+ public int hashCode() {
+ return wrapped.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // defer to reference's equals() logic.
+ return obj.equals(this);
+ }
+ }
+
+ /**
+ * Used for keys. Overrides hash code to use identity hash code.
+ */
+ static class KeyReferenceAwareWrapper extends ReferenceAwareWrapper {
+
+ public KeyReferenceAwareWrapper(Object wrapped) {
+ super(wrapped);
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(wrapped);
+ }
+ }
+
+ class SoftKeyReference extends FinalizableSoftReference<Object> implements InternalReference {
+
+ int hashCode;
+
+ public SoftKeyReference(Object key) {
+ super(key);
+ this.hashCode = keyHashCode(key);
+ }
+
+ public void finalizeReferent() {
+ delegate.remove(this);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return referenceEquals(this, o);
+ }
+ }
+
+ class WeakKeyReference extends FinalizableWeakReference<Object> implements InternalReference {
+
+ int hashCode;
+
+ public WeakKeyReference(Object key) {
+ super(key);
+ this.hashCode = keyHashCode(key);
+ }
+
+ public void finalizeReferent() {
+ delegate.remove(this);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.hashCode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return referenceEquals(this, o);
+ }
+ }
+
+ class SoftValueReference extends FinalizableSoftReference<Object> implements InternalReference {
+
+ Object keyReference;
+
+ public SoftValueReference(Object keyReference, Object value) {
+ super(value);
+ this.keyReference = keyReference;
+ }
+
+ public void finalizeReferent() {
+ delegate.remove(keyReference, this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return referenceEquals(this, obj);
+ }
+ }
+
+ class WeakValueReference extends FinalizableWeakReference<Object> implements InternalReference {
+
+ Object keyReference;
+
+ public WeakValueReference(Object keyReference, Object value) {
+ super(value);
+ this.keyReference = keyReference;
+ }
+
+ public void finalizeReferent() {
+ delegate.remove(keyReference, this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return referenceEquals(this, obj);
+ }
+ }
+
+ protected interface Strategy {
+ public Object execute(ReferenceMap map, Object keyReference, Object valueReference);
+ }
+
+ protected Strategy putStrategy() {
+ return PutStrategy.PUT;
+ }
+
+ protected Strategy putIfAbsentStrategy() {
+ return PutStrategy.PUT_IF_ABSENT;
+ }
+
+ protected Strategy replaceStrategy() {
+ return PutStrategy.REPLACE;
+ }
+
+ private enum PutStrategy implements Strategy {
+ PUT {
+ public Object execute(ReferenceMap map, Object keyReference, Object valueReference) {
+ return map.delegate.put(keyReference, valueReference);
+ }
+ },
+
+ REPLACE {
+ public Object execute(ReferenceMap map, Object keyReference, Object valueReference) {
+ return map.delegate.replace(keyReference, valueReference);
+ }
+ },
+
+ PUT_IF_ABSENT {
+ public Object execute(ReferenceMap map, Object keyReference, Object valueReference) {
+ return map.delegate.putIfAbsent(keyReference, valueReference);
+ }
+ };
+ }
+
+ private static PutStrategy defaultPutStrategy;
+
+ protected PutStrategy getPutStrategy() {
+ return defaultPutStrategy;
+ }
+
+
+ class Entry implements Map.Entry<K, V> {
+
+ K key;
+ V value;
+
+ public Entry(K key, V value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public K getKey() {
+ return this.key;
+ }
+
+ public V getValue() {
+ return this.value;
+ }
+
+ public V setValue(V value) {
+ return put(key, value);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode() * 31 + value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ReferenceMap.Entry)) {
+ return false;
+ }
+
+ Entry entry = (Entry) o;
+ return key.equals(entry.key) && value.equals(entry.value);
+ }
+
+ @Override
+ public String toString() {
+ return key + "=" + value;
+ }
+ }
+
+ static void ensureNotNull(Object o) {
+ if (o == null) {
+ throw new NullPointerException();
+ }
+ }
+
+ static void ensureNotNull(Object... array) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i] == null) {
+ throw new NullPointerException("Argument #" + i + " is null.");
+ }
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ out.writeInt(size());
+ for (Map.Entry<Object, Object> entry : delegate.entrySet()) {
+ Object key = dereferenceKey(entry.getKey());
+ Object value = dereferenceValue(entry.getValue());
+
+ // don't persist gc'ed entries.
+ if (key != null && value != null) {
+ out.writeObject(key);
+ out.writeObject(value);
+ }
+ }
+ out.writeObject(null);
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+ int size = in.readInt();
+ this.delegate = new ConcurrentHashMap<Object, Object>(size);
+ while (true) {
+ K key = (K) in.readObject();
+ if (key == null) {
+ break;
+ }
+ V value = (V) in.readObject();
+ put(key, value);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceType.java b/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceType.java
new file mode 100644
index 0000000..a223a00
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/inject/util/ReferenceType.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2006 Google Inc.
+ *
+ * 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 com.opensymphony.xwork2.inject.util;
+
+/**
+ * Reference type. Used to specify what type of reference to keep to a
+ * referent.
+ *
+ * @see java.lang.ref.Reference
+ * @author crazybob@google.com (Bob Lee)
+ */
+public enum ReferenceType {
+
+ /**
+ * Prevents referent from being reclaimed by the garbage collector.
+ */
+ STRONG,
+
+ /**
+ * Referent reclaimed in an LRU fashion when the VM runs low on memory and
+ * no strong references exist.
+ *
+ * @see java.lang.ref.SoftReference
+ */
+ SOFT,
+
+ /**
+ * Referent reclaimed when no strong or soft references exist.
+ *
+ * @see java.lang.ref.WeakReference
+ */
+ WEAK,
+
+ /**
+ * Similar to weak references except the garbage collector doesn't actually
+ * reclaim the referent. More flexible alternative to finalization.
+ *
+ * @see java.lang.ref.PhantomReference
+ */
+ PHANTOM;
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/util/Strings.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/util/Strings.java b/core/src/main/java/com/opensymphony/xwork2/inject/util/Strings.java
new file mode 100644
index 0000000..a15291a
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/inject/util/Strings.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (C) 2006 Google Inc.
+ *
+ * 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 com.opensymphony.xwork2.inject.util;
+
+/**
+ * String utilities.
+ *
+ * @author crazybob@google.com (Bob Lee)
+ */
+public class Strings {
+
+ /**
+ * Returns a string that is equivalent to the specified string with its
+ * first character converted to uppercase as by {@link String#toUpperCase}.
+ * The returned string will have the same value as the specified string if
+ * its first character is non-alphabetic, if its first character is already
+ * uppercase, or if the specified string is of length 0.
+ *
+ * <p>For example:
+ * <pre>
+ * capitalize("foo bar").equals("Foo bar");
+ * capitalize("2b or not 2b").equals("2b or not 2b")
+ * capitalize("Foo bar").equals("Foo bar");
+ * capitalize("").equals("");
+ * </pre>
+ *
+ * @param s the string whose first character is to be uppercased
+ * @return a string equivalent to <tt>s</tt> with its first character
+ * converted to uppercase
+ * @throws NullPointerException if <tt>s</tt> is null
+ */
+ public static String capitalize(String s) {
+ if (s.length() == 0)
+ return s;
+ char first = s.charAt(0);
+ char capitalized = Character.toUpperCase(first);
+ return (first == capitalized)
+ ? s
+ : capitalized + s.substring(1);
+ }
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/inject/util/package.html
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/inject/util/package.html b/core/src/main/java/com/opensymphony/xwork2/inject/util/package.html
new file mode 100644
index 0000000..e326461
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/inject/util/package.html
@@ -0,0 +1 @@
+<body>Guice util classes.</body>
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/AbstractInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/AbstractInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/AbstractInterceptor.java
new file mode 100644
index 0000000..470d349
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/AbstractInterceptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2006,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionInvocation;
+
+/**
+ * Provides default implementations of optional lifecycle methods
+ */
+public abstract class AbstractInterceptor implements Interceptor {
+
+ /**
+ * Does nothing
+ */
+ public void init() {
+ }
+
+ /**
+ * Does nothing
+ */
+ public void destroy() {
+ }
+
+
+ /**
+ * Override to handle interception
+ */
+ public abstract String intercept(ActionInvocation invocation) throws Exception;
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/AliasInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/AliasInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/AliasInterceptor.java
new file mode 100644
index 0000000..7bd9499
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/AliasInterceptor.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2002-2006,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.XWorkConstants;
+import com.opensymphony.xwork2.config.entities.ActionConfig;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.ClearableValueStack;
+import com.opensymphony.xwork2.util.LocalizedTextUtil;
+import com.opensymphony.xwork2.util.ValueStack;
+import com.opensymphony.xwork2.util.ValueStackFactory;
+import com.opensymphony.xwork2.util.reflection.ReflectionContextState;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Map;
+
+
+/**
+ * <!-- START SNIPPET: description -->
+ *
+ * The aim of this Interceptor is to alias a named parameter to a different named parameter. By acting as the glue
+ * between actions sharing similiar parameters (but with different names), it can help greatly with action chaining.
+ *
+ * <p/> Action's alias expressions should be in the form of <code>#{ "name1" : "alias1", "name2" : "alias2" }</code>.
+ * This means that assuming an action (or something else in the stack) has a value for the expression named <i>name1</i> and the
+ * action this interceptor is applied to has a setter named <i>alias1</i>, <i>alias1</i> will be set with the value from
+ * <i>name1</i>.
+ *
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Interceptor parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ *
+ * <ul>
+ *
+ * <li>aliasesKey (optional) - the name of the action parameter to look for the alias map (by default this is
+ * <i>aliases</i>).</li>
+ *
+ * </ul>
+ *
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Extending the interceptor:</u>
+ *
+ * <p/>
+ *
+ * <!-- START SNIPPET: extending -->
+ *
+ * This interceptor does not have any known extension points.
+ *
+ * <!-- END SNIPPET: extending -->
+ *
+ * <p/> <u>Example code:</u>
+ *
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <!-- The value for the foo parameter will be applied as if it were named bar -->
+ * <param name="aliases">#{ 'foo' : 'bar' }</param>
+ *
+ * <interceptor-ref name="alias"/>
+ * <interceptor-ref name="basicStack"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Matthew Payne
+ */
+public class AliasInterceptor extends AbstractInterceptor {
+
+ private static final Logger LOG = LogManager.getLogger(AliasInterceptor.class);
+
+ private static final String DEFAULT_ALIAS_KEY = "aliases";
+ protected String aliasesKey = DEFAULT_ALIAS_KEY;
+
+ protected ValueStackFactory valueStackFactory;
+ static boolean devMode = false;
+
+ @Inject(XWorkConstants.DEV_MODE)
+ public static void setDevMode(String mode) {
+ devMode = "true".equals(mode);
+ }
+
+ @Inject
+ public void setValueStackFactory(ValueStackFactory valueStackFactory) {
+ this.valueStackFactory = valueStackFactory;
+ }
+
+ /**
+ * Sets the name of the action parameter to look for the alias map.
+ * <p/>
+ * Default is <code>aliases</code>.
+ *
+ * @param aliasesKey the name of the action parameter
+ */
+ public void setAliasesKey(String aliasesKey) {
+ this.aliasesKey = aliasesKey;
+ }
+
+ @Override public String intercept(ActionInvocation invocation) throws Exception {
+
+ ActionConfig config = invocation.getProxy().getConfig();
+ ActionContext ac = invocation.getInvocationContext();
+ Object action = invocation.getAction();
+
+ // get the action's parameters
+ final Map<String, String> parameters = config.getParams();
+
+ if (parameters.containsKey(aliasesKey)) {
+
+ String aliasExpression = parameters.get(aliasesKey);
+ ValueStack stack = ac.getValueStack();
+ Object obj = stack.findValue(aliasExpression);
+
+ if (obj != null && obj instanceof Map) {
+ //get secure stack
+ ValueStack newStack = valueStackFactory.createValueStack(stack);
+ boolean clearableStack = newStack instanceof ClearableValueStack;
+ if (clearableStack) {
+ //if the stack's context can be cleared, do that to prevent OGNL
+ //from having access to objects in the stack, see XW-641
+ ((ClearableValueStack)newStack).clearContextValues();
+ Map<String, Object> context = newStack.getContext();
+ ReflectionContextState.setCreatingNullObjects(context, true);
+ ReflectionContextState.setDenyMethodExecution(context, true);
+ ReflectionContextState.setReportingConversionErrors(context, true);
+
+ //keep locale from original context
+ context.put(ActionContext.LOCALE, stack.getContext().get(ActionContext.LOCALE));
+ }
+
+ // override
+ Map aliases = (Map) obj;
+ for (Object o : aliases.entrySet()) {
+ Map.Entry entry = (Map.Entry) o;
+ String name = entry.getKey().toString();
+ String alias = (String) entry.getValue();
+ Object value = stack.findValue(name);
+ if (null == value) {
+ // workaround
+ Map<String, Object> contextParameters = ActionContext.getContext().getParameters();
+
+ if (null != contextParameters) {
+ value = contextParameters.get(name);
+ }
+ }
+ if (null != value) {
+ try {
+ newStack.setValue(alias, value);
+ } catch (RuntimeException e) {
+ if (devMode) {
+ String developerNotification = LocalizedTextUtil.findText(ParametersInterceptor.class, "devmode.notification", ActionContext.getContext().getLocale(), "Developer Notification:\n{0}", new Object[]{
+ "Unexpected Exception caught setting '" + entry.getKey() + "' on '" + action.getClass() + ": " + e.getMessage()
+ });
+ LOG.error(developerNotification);
+ if (action instanceof ValidationAware) {
+ ((ValidationAware) action).addActionMessage(developerNotification);
+ }
+ }
+ }
+ }
+ }
+
+ if (clearableStack && (stack.getContext() != null) && (newStack.getContext() != null))
+ stack.getContext().put(ActionContext.CONVERSION_ERRORS, newStack.getContext().get(ActionContext.CONVERSION_ERRORS));
+ } else {
+ LOG.debug("invalid alias expression: {}", aliasesKey);
+ }
+ }
+
+ return invocation.invoke();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/ChainingInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/ChainingInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/ChainingInterceptor.java
new file mode 100644
index 0000000..8218e1b
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/ChainingInterceptor.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2002-2007,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionChainResult;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.Result;
+import com.opensymphony.xwork2.Unchainable;
+import com.opensymphony.xwork2.inject.Inject;
+import com.opensymphony.xwork2.util.CompoundRoot;
+import com.opensymphony.xwork2.util.ValueStack;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import com.opensymphony.xwork2.util.reflection.ReflectionProvider;
+
+import java.util.*;
+
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>
+ * An interceptor that copies all the properties of every object in the value stack to the currently executing object,
+ * except for any object that implements {@link Unchainable}. A collection of optional <i>includes</i> and
+ * <i>excludes</i> may be provided to control how and which parameters are copied. Only includes or excludes may be
+ * specified. Specifying both results in undefined behavior. See the javadocs for {@link ReflectionProvider#copy(Object, Object,
+ * java.util.Map, java.util.Collection, java.util.Collection)} for more information.
+ * <p/>
+ * <p/>
+ * <b>Note:</b> It is important to remember that this interceptor does nothing if there are no objects already on the stack.
+ * <br/>This means two things:
+ * <br/><b>One</b>, you can safely apply it to all your actions without any worry of adverse affects.
+ * <br/><b/>Two</b>, it is up to you to ensure an object exists in the stack prior to invoking this action. The most typical way this is done
+ * is through the use of the <b>chain</b> result type, which combines with this interceptor to make up the action
+ * chaining feature.
+ * <p/>
+ * <b>Note:</b> By default Errors, Field errors and Message aren't copied during chaining, to change the behaviour you can specify
+ * the below three constants in struts.properties or struts.xml:
+ * <ul>
+ * <li>struts.xwork.chaining.copyErrors - set to true to copy Action Errors</li>
+ * <li>struts.xwork.chaining.copyFieldErrors - set to true to copy Field Errors</li>
+ * <li>struts.xwork.chaining.copyMessages - set to true to copy Action Messages</li>
+ * </ul>
+ * <p>
+ * <p>
+ * <u>Example:</u>
+ * <pre>
+ * <constant name="struts.xwork.chaining.copyErrors" value="true"/>
+ * </pre>
+ * </p>
+ * <b>Note:</b> By default actionErrors and actionMessages are excluded when copping object's properties.
+ * </p>
+ * <!-- END SNIPPET: description -->
+ * <u>Interceptor parameters:</u>
+ * <!-- START SNIPPET: parameters -->
+ * <ul>
+ * <li>excludes (optional) - the list of parameter names to exclude from copying (all others will be included).</li>
+ * <li>includes (optional) - the list of parameter names to include when copying (all others will be excluded).</li>
+ * </ul>
+ * <!-- END SNIPPET: parameters -->
+ * <u>Extending the interceptor:</u>
+ * <!-- START SNIPPET: extending -->
+ * <p>
+ * There are no known extension points to this interceptor.
+ * </p>
+ * <!-- END SNIPPET: extending -->
+ * <u>Example code:</u>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * <p/>
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="basicStack"/>
+ * <result name="success" type="chain">otherAction</result>
+ * </action>
+ * <p/>
+ * <action name="otherAction" class="com.examples.OtherAction">
+ * <interceptor-ref name="chain"/>
+ * <interceptor-ref name="basicStack"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ * <p/>
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author mrdon
+ * @author tm_jee ( tm_jee(at)yahoo.co.uk )
+ * @see com.opensymphony.xwork2.ActionChainResult
+ */
+public class ChainingInterceptor extends AbstractInterceptor {
+
+ private static final Logger LOG = LogManager.getLogger(ChainingInterceptor.class);
+
+ private static final String ACTION_ERRORS = "actionErrors";
+ private static final String FIELD_ERRORS = "fieldErrors";
+ private static final String ACTION_MESSAGES = "actionMessages";
+
+ private boolean copyMessages = false;
+ private boolean copyErrors = false;
+ private boolean copyFieldErrors = false;
+
+ protected Collection<String> excludes;
+
+ protected Collection<String> includes;
+ protected ReflectionProvider reflectionProvider;
+
+ @Inject
+ public void setReflectionProvider(ReflectionProvider prov) {
+ this.reflectionProvider = prov;
+ }
+
+ @Inject(value = "struts.xwork.chaining.copyErrors", required = false)
+ public void setCopyErrors(String copyErrors) {
+ this.copyErrors = "true".equalsIgnoreCase(copyErrors);
+ }
+
+ @Inject(value = "struts.xwork.chaining.copyFieldErrors", required = false)
+ public void setCopyFieldErrors(String copyFieldErrors) {
+ this.copyFieldErrors = "true".equalsIgnoreCase(copyFieldErrors);
+ }
+
+ @Inject(value = "struts.xwork.chaining.copyMessages", required = false)
+ public void setCopyMessages(String copyMessages) {
+ this.copyMessages = "true".equalsIgnoreCase(copyMessages);
+ }
+
+ @Override
+ public String intercept(ActionInvocation invocation) throws Exception {
+ ValueStack stack = invocation.getStack();
+ CompoundRoot root = stack.getRoot();
+ if (shouldCopyStack(invocation, root)) {
+ copyStack(invocation, root);
+ }
+ return invocation.invoke();
+ }
+
+ private void copyStack(ActionInvocation invocation, CompoundRoot root) {
+ List list = prepareList(root);
+ Map<String, Object> ctxMap = invocation.getInvocationContext().getContextMap();
+ for (Object object : list) {
+ if (shouldCopy(object)) {
+ reflectionProvider.copy(object, invocation.getAction(), ctxMap, prepareExcludes(), includes);
+ }
+ }
+ }
+
+ private Collection<String> prepareExcludes() {
+ Collection<String> localExcludes = excludes;
+ if (!copyErrors || !copyMessages ||!copyFieldErrors) {
+ if (localExcludes == null) {
+ localExcludes = new HashSet<String>();
+ if (!copyErrors) {
+ localExcludes.add(ACTION_ERRORS);
+ }
+ if (!copyMessages) {
+ localExcludes.add(ACTION_MESSAGES);
+ }
+ if (!copyFieldErrors) {
+ localExcludes.add(FIELD_ERRORS);
+ }
+ }
+ }
+ return localExcludes;
+ }
+
+ private boolean shouldCopy(Object o) {
+ return o != null && !(o instanceof Unchainable);
+ }
+
+ @SuppressWarnings("unchecked")
+ private List prepareList(CompoundRoot root) {
+ List list = new ArrayList(root);
+ list.remove(0);
+ Collections.reverse(list);
+ return list;
+ }
+
+ private boolean shouldCopyStack(ActionInvocation invocation, CompoundRoot root) throws Exception {
+ Result result = invocation.getResult();
+ return root.size() > 1 && (result == null || ActionChainResult.class.isAssignableFrom(result.getClass()));
+ }
+
+ /**
+ * Gets list of parameter names to exclude
+ *
+ * @return the exclude list
+ */
+ public Collection<String> getExcludes() {
+ return excludes;
+ }
+
+ /**
+ * Sets the list of parameter names to exclude from copying (all others will be included).
+ *
+ * @param excludes the excludes list
+ */
+ public void setExcludes(Collection<String> excludes) {
+ this.excludes = excludes;
+ }
+
+ /**
+ * Gets list of parameter names to include
+ *
+ * @return the include list
+ */
+ public Collection<String> getIncludes() {
+ return includes;
+ }
+
+ /**
+ * Sets the list of parameter names to include when copying (all others will be excluded).
+ *
+ * @param includes the includes list
+ */
+ public void setIncludes(Collection<String> includes) {
+ this.includes = includes;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java
new file mode 100644
index 0000000..47d020a
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/ConversionErrorInterceptor.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2002-2007,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
+import com.opensymphony.xwork2.util.ValueStack;
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * <!-- START SNIPPET: description -->
+ * ConversionErrorInterceptor adds conversion errors from the ActionContext to the Action's field errors.
+ *
+ * <p/>
+ * This interceptor adds any error found in the {@link ActionContext}'s conversionErrors map as a field error (provided
+ * that the action implements {@link ValidationAware}). In addition, any field that contains a validation error has its
+ * original value saved such that any subsequent requests for that value return the original value rather than the value
+ * in the action. This is important because if the value "abc" is submitted and can't be converted to an int, we want to
+ * display the original string ("abc") again rather than the int value (likely 0, which would make very little sense to
+ * the user).
+ *
+ *
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Interceptor parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ *
+ * <ul>
+ *
+ * <li>None</li>
+ *
+ * </ul>
+ *
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Extending the interceptor:</u>
+ *
+ * <p/>
+ *
+ * <!-- START SNIPPET: extending -->
+ *
+ * Because this interceptor is not web-specific, it abstracts the logic for whether an error should be added. This
+ * allows for web-specific interceptors to use more complex logic in the {@link #shouldAddError} method for when a value
+ * has a conversion error but is null or empty or otherwise indicates that the value was never actually entered by the
+ * user.
+ *
+ * <!-- END SNIPPET: extending -->
+ *
+ * <p/> <u>Example code:</u>
+ *
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="conversionError"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Jason Carreira
+ */
+public class ConversionErrorInterceptor extends AbstractInterceptor {
+
+ public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override";
+
+ protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
+ return escape(value);
+ }
+
+ protected String escape(Object value) {
+ return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\"";
+ }
+
+ @Override
+ public String intercept(ActionInvocation invocation) throws Exception {
+
+ ActionContext invocationContext = invocation.getInvocationContext();
+ Map<String, Object> conversionErrors = invocationContext.getConversionErrors();
+ ValueStack stack = invocationContext.getValueStack();
+
+ HashMap<Object, Object> fakie = null;
+
+ for (Map.Entry<String, Object> entry : conversionErrors.entrySet()) {
+ String propertyName = entry.getKey();
+ Object value = entry.getValue();
+
+ if (shouldAddError(propertyName, value)) {
+ String message = XWorkConverter.getConversionErrorMessage(propertyName, stack);
+
+ Object action = invocation.getAction();
+ if (action instanceof ValidationAware) {
+ ValidationAware va = (ValidationAware) action;
+ va.addFieldError(propertyName, message);
+ }
+
+ if (fakie == null) {
+ fakie = new HashMap<>();
+ }
+
+ fakie.put(propertyName, getOverrideExpr(invocation, value));
+ }
+ }
+
+ if (fakie != null) {
+ // if there were some errors, put the original (fake) values in place right before the result
+ stack.getContext().put(ORIGINAL_PROPERTY_OVERRIDE, fakie);
+ invocation.addPreResultListener(new PreResultListener() {
+ public void beforeResult(ActionInvocation invocation, String resultCode) {
+ Map<Object, Object> fakie = (Map<Object, Object>) invocation.getInvocationContext().get(ORIGINAL_PROPERTY_OVERRIDE);
+
+ if (fakie != null) {
+ invocation.getStack().setExprOverrides(fakie);
+ }
+ }
+ });
+ }
+ return invocation.invoke();
+ }
+
+ protected boolean shouldAddError(String propertyName, Object value) {
+ return true;
+ }
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/DefaultWorkflowInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/DefaultWorkflowInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/DefaultWorkflowInterceptor.java
new file mode 100644
index 0000000..13cea0e
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/DefaultWorkflowInterceptor.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2002-2007,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.Action;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.ValidationAware;
+import com.opensymphony.xwork2.interceptor.annotations.InputConfig;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.lang.reflect.Method;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>
+ * An interceptor that makes sure there are not validation errors before allowing the interceptor chain to continue.
+ * <b>This interceptor does not perform any validation</b>.
+ * <p/>
+ * This interceptor does nothing if the name of the method being invoked is specified in the <b>excludeMethods</b>
+ * parameter. <b>excludeMethods</b> accepts a comma-delimited list of method names. For example, requests to
+ * <b>foo!input.action</b> and <b>foo!back.action</b> will be skipped by this interceptor if you set the
+ * <b>excludeMethods</b> parameter to "input, back".
+ * <p/>
+ * <b>Note:</b> As this method extends off MethodFilterInterceptor, it is capable of
+ * deciding if it is applicable only to selective methods in the action class. This is done by adding param tags
+ * for the interceptor element, naming either a list of excluded method names and/or a list of included method
+ * names, whereby includeMethods overrides excludedMethods. A single * sign is interpreted as wildcard matching
+ * all methods for both parameters.
+ * See {@link MethodFilterInterceptor} for more info.
+ * <p/>
+ * This interceptor also supports the following interfaces which can implemented by actions:
+ * <ul>
+ * <li>ValidationAware - implemented by ActionSupport class</li>
+ * <li>ValidationWorkflowAware - allows changing result name programmatically</li>
+ * <li>ValidationErrorAware - notifies action about errors and also allow change result name</li>
+ * </ul>
+ *
+ * You can also use InputConfig annotation to change result name returned when validation errors occurred.
+ *
+ * <!-- END SNIPPET: description -->
+ *
+ * <u>Interceptor parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ * <ul>
+ * <li>inputResultName - Default to "input". Determine the result name to be returned when
+ * an action / field error is found.</li>
+ * </ul>
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <u>Extending the interceptor:</u>
+ *
+ * <!-- START SNIPPET: extending -->
+ *
+ * There are no known extension points for this interceptor.
+ *
+ * <!-- END SNIPPET: extending -->
+ *
+ * <u>Example code:</u>
+ *
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ *
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <-- In this case myMethod as well as mySecondMethod of the action class
+ * will not pass through the workflow process -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow">
+ * <param name="excludeMethods">myMethod,mySecondMethod</param>
+ * </interceptor-ref name="workflow">
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <-- In this case, the result named "error" will be used when
+ * an action / field error is found -->
+ * <-- The Interceptor will only be applied for myWorkflowMethod method of action
+ * classes, since this is the only included method while any others are excluded -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation"/>
+ * <interceptor-ref name="workflow">
+ * <param name="inputResultName">error</param>
+ * <param name="excludeMethods">*</param>
+ * <param name="includeMethods">myWorkflowMethod</param>
+ * </interceptor-ref>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ *
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Jason Carreira
+ * @author Rainer Hermanns
+ * @author <a href='mailto:the_mindstorm[at]evolva[dot]ro'>Alexandru Popescu</a>
+ * @author Philip Luppens
+ * @author tm_jee
+ */
+public class DefaultWorkflowInterceptor extends MethodFilterInterceptor {
+
+ private static final long serialVersionUID = 7563014655616490865L;
+
+ private static final Logger LOG = LogManager.getLogger(DefaultWorkflowInterceptor.class);
+
+ private static final Class[] EMPTY_CLASS_ARRAY = new Class[0];
+
+ private String inputResultName = Action.INPUT;
+
+ /**
+ * Set the <code>inputResultName</code> (result name to be returned when
+ * a action / field error is found registered). Default to {@link Action#INPUT}
+ *
+ * @param inputResultName what result name to use when there was validation error(s).
+ */
+ public void setInputResultName(String inputResultName) {
+ this.inputResultName = inputResultName;
+ }
+
+ /**
+ * Intercept {@link ActionInvocation} and returns a <code>inputResultName</code>
+ * when action / field errors is found registered.
+ *
+ * @return String result name
+ */
+ @Override
+ protected String doIntercept(ActionInvocation invocation) throws Exception {
+ Object action = invocation.getAction();
+
+ if (action instanceof ValidationAware) {
+ ValidationAware validationAwareAction = (ValidationAware) action;
+
+ if (validationAwareAction.hasErrors()) {
+ LOG.debug("Errors on action [{}], returning result name [{}]", validationAwareAction, inputResultName);
+
+ String resultName = inputResultName;
+ resultName = processValidationWorkflowAware(action, resultName);
+ resultName = processInputConfig(action, invocation.getProxy().getMethod(), resultName);
+ resultName = processValidationErrorAware(action, resultName);
+
+ return resultName;
+ }
+ }
+
+ return invocation.invoke();
+ }
+
+ /**
+ * Process {@link ValidationWorkflowAware} interface
+ */
+ private String processValidationWorkflowAware(final Object action, final String currentResultName) {
+ String resultName = currentResultName;
+ if (action instanceof ValidationWorkflowAware) {
+ resultName = ((ValidationWorkflowAware) action).getInputResultName();
+ LOG.debug("Changing result name from [{}] to [{}] because of processing [{}] interface applied to [{}]",
+ currentResultName, resultName, InputConfig.class.getSimpleName(), ValidationWorkflowAware.class.getSimpleName(), action);
+ }
+ return resultName;
+ }
+
+ /**
+ * Process {@link InputConfig} annotation applied to method
+ */
+ protected String processInputConfig(final Object action, final String method, final String currentResultName) throws Exception {
+ String resultName = currentResultName;
+ InputConfig annotation = action.getClass().getMethod(method, EMPTY_CLASS_ARRAY).getAnnotation(InputConfig.class);
+ if (annotation != null) {
+ if (StringUtils.isNotEmpty(annotation.methodName())) {
+ Method m = action.getClass().getMethod(annotation.methodName());
+ resultName = (String) m.invoke(action);
+ } else {
+ resultName = annotation.resultName();
+ }
+ LOG.debug("Changing result name from [{}] to [{}] because of processing annotation [{}] on action [{}]",
+ currentResultName, resultName, InputConfig.class.getSimpleName(), action);
+ }
+ return resultName;
+ }
+
+ /**
+ * Notify action if it implements {@see ValidationErrorAware} interface
+ */
+ protected String processValidationErrorAware(final Object action, final String currentResultName) {
+ String resultName = currentResultName;
+ if (action instanceof ValidationErrorAware) {
+ resultName = ((ValidationErrorAware) action).actionErrorOccurred(currentResultName);
+ LOG.debug("Changing result name from [{}] to [{}] because of processing interface [{}] on action [{}]",
+ currentResultName, resultName, ValidationErrorAware.class.getSimpleName(), action);
+ }
+ return resultName;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionHolder.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionHolder.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionHolder.java
new file mode 100644
index 0000000..6959357
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionHolder.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2007,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Serializable;
+
+/**
+ * <!-- START SNIPPET: javadoc -->
+ *
+ * A simple wrapper around an exception, providing an easy way to print out the stack trace of the exception as well as
+ * a way to get a handle on the exception itself.
+ *
+ * <!-- END SNIPPET: javadoc -->
+ *
+ * @author Matthew E. Porter (matthew dot porter at metissian dot com)
+ */
+public class ExceptionHolder implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ private Exception exception;
+
+ /**
+ * Holds the given exception
+ *
+ * @param exception the exception to hold.
+ */
+ public ExceptionHolder(Exception exception) {
+ this.exception = exception;
+ }
+
+ /**
+ * Gets the held exception
+ *
+ * @return the held exception
+ */
+ public Exception getException() {
+ return this.exception;
+ }
+
+ /**
+ * Gets the held exception stack trace using {@link Exception#printStackTrace()}.
+ *
+ * @return stack trace
+ */
+ public String getExceptionStack() {
+ String exceptionStack = null;
+
+ if (getException() != null) {
+ try (StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw)) {
+ getException().printStackTrace(pw);
+ exceptionStack = sw.toString();
+ } catch (IOException e) {
+ // Ignore exception generating stack trace.
+ }
+ }
+
+ return exceptionStack;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionMappingInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionMappingInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionMappingInterceptor.java
new file mode 100644
index 0000000..d1a5319
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/ExceptionMappingInterceptor.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright 2002-2006,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <!-- START SNIPPET: description -->
+ *
+ * This interceptor forms the core functionality of the exception handling feature. Exception handling allows you to map
+ * an exception to a result code, just as if the action returned a result code instead of throwing an unexpected
+ * exception. When an exception is encountered, it is wrapped with an {@link ExceptionHolder} and pushed on the stack,
+ * providing easy access to the exception from within your result.
+ *
+ * <b>Note:</b> While you can configure exception mapping in your configuration file at any point, the configuration
+ * will not have any effect if this interceptor is not in the interceptor stack for your actions. It is recommended that
+ * you make this interceptor the first interceptor on the stack, ensuring that it has full access to catch any
+ * exception, even those caused by other interceptors.
+ *
+ * <!-- END SNIPPET: description -->
+ *
+ * <p/> <u>Interceptor parameters:</u>
+ *
+ * <!-- START SNIPPET: parameters -->
+ *
+ * <ul>
+ *
+ * <li>logEnabled (optional) - Should exceptions also be logged? (boolean true|false)</li>
+ *
+ * <li>logLevel (optional) - what log level should we use (<code>trace, debug, info, warn, error, fatal</code>)? - defaut is <code>debug</code></li>
+ *
+ * <li>logCategory (optional) - If provided we would use this category (eg. <code>com.mycompany.app</code>).
+ * Default is to use <code>com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor</code>.</li>
+ *
+ * </ul>
+ *
+ * The parameters above enables us to log all thrown exceptions with stacktace in our own logfile,
+ * and present a friendly webpage (with no stacktrace) to the end user.
+ *
+ * <!-- END SNIPPET: parameters -->
+ *
+ * <p/> <u>Extending the interceptor:</u>
+ *
+ * <p/>
+ *
+ * <!-- START SNIPPET: extending -->
+ *
+ * If you want to add custom handling for publishing the Exception, you may override
+ * {@link #publishException(com.opensymphony.xwork2.ActionInvocation, ExceptionHolder)}. The default implementation
+ * pushes the given ExceptionHolder on value stack. A custom implementation could add additional logging etc.
+ *
+ * <!-- END SNIPPET: extending -->
+ *
+ * <p/> <u>Example code:</u>
+ *
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * <xwork>
+ * <package name="default" extends="xwork-default">
+ * <global-results>
+ * <result name="error" type="freemarker">error.ftl</result>
+ * </global-results>
+ *
+ * <global-exception-mappings>
+ * <exception-mapping exception="java.lang.Exception" result="error"/>
+ * </global-exception-mappings>
+ *
+ * <action name="test">
+ * <interceptor-ref name="exception"/>
+ * <interceptor-ref name="basicStack"/>
+ * <exception-mapping exception="com.acme.CustomException" result="custom_error"/>
+ * <result name="custom_error">custom_error.ftl</result>
+ * <result name="success" type="freemarker">test.ftl</result>
+ * </action>
+ * </package>
+ * </xwork>
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * <p/>
+ * This second example will also log the exceptions using our own category
+ * <code>com.mycompany.app.unhandled<code> at WARN level.
+ *
+ * <pre>
+ * <!-- START SNIPPET: example2 -->
+ * <xwork>
+ * <package name="something" extends="xwork-default">
+ * <interceptors>
+ * <interceptor-stack name="exceptionmappingStack">
+ * <interceptor-ref name="exception">
+ * <param name="logEnabled">true</param>
+ * <param name="logCategory">com.mycompany.app.unhandled</param>
+ * <param name="logLevel">WARN</param>
+ * </interceptor-ref>
+ * <interceptor-ref name="i18n"/>
+ * <interceptor-ref name="staticParams"/>
+ * <interceptor-ref name="params"/>
+ * <interceptor-ref name="validation">
+ * <param name="excludeMethods">input,back,cancel,browse</param>
+ * </interceptor-ref>
+ * </interceptor-stack>
+ * </interceptors>
+ *
+ * <default-interceptor-ref name="exceptionmappingStack"/>
+ *
+ * <global-results>
+ * <result name="unhandledException">/unhandled-exception.jsp</result>
+ * </global-results>
+ *
+ * <global-exception-mappings>
+ * <exception-mapping exception="java.lang.Exception" result="unhandledException"/>
+ * </global-exception-mappings>
+ *
+ * <action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction">
+ * <exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException"
+ * result="damm"/>
+ * <result name="input">index.jsp</result>
+ * <result name="success">success.jsp</result>
+ * <result name="damm">damm.jsp</result>
+ * </action>
+ *
+ * </package>
+ * </xwork>
+ * <!-- END SNIPPET: example2 -->
+ * </pre>
+ *
+ * @author Matthew E. Porter (matthew dot porter at metissian dot com)
+ * @author Claus Ibsen
+ */
+public class ExceptionMappingInterceptor extends AbstractInterceptor {
+
+ protected static final Logger LOG = LogManager.getLogger(ExceptionMappingInterceptor.class);
+
+ protected Logger categoryLogger;
+ protected boolean logEnabled = false;
+ protected String logCategory;
+ protected String logLevel;
+
+
+ public boolean isLogEnabled() {
+ return logEnabled;
+ }
+
+ public void setLogEnabled(boolean logEnabled) {
+ this.logEnabled = logEnabled;
+ }
+
+ public String getLogCategory() {
+ return logCategory;
+ }
+
+ public void setLogCategory(String logCatgory) {
+ this.logCategory = logCatgory;
+ }
+
+ public String getLogLevel() {
+ return logLevel;
+ }
+
+ public void setLogLevel(String logLevel) {
+ this.logLevel = logLevel;
+ }
+
+ @Override
+ public String intercept(ActionInvocation invocation) throws Exception {
+ String result;
+
+ try {
+ result = invocation.invoke();
+ } catch (Exception e) {
+ if (isLogEnabled()) {
+ handleLogging(e);
+ }
+ List<ExceptionMappingConfig> exceptionMappings = invocation.getProxy().getConfig().getExceptionMappings();
+ ExceptionMappingConfig mappingConfig = this.findMappingFromExceptions(exceptionMappings, e);
+ if (mappingConfig != null && mappingConfig.getResult()!=null) {
+ Map parameterMap = mappingConfig.getParams();
+ // create a mutable HashMap since some interceptors will remove parameters, and parameterMap is immutable
+ invocation.getInvocationContext().setParameters(new HashMap<String, Object>(parameterMap));
+ result = mappingConfig.getResult();
+ publishException(invocation, new ExceptionHolder(e));
+ } else {
+ throw e;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Handles the logging of the exception.
+ *
+ * @param e the exception to log.
+ */
+ protected void handleLogging(Exception e) {
+ if (logCategory != null) {
+ if (categoryLogger == null) {
+ // init category logger
+ categoryLogger = LogManager.getLogger(logCategory);
+ }
+ doLog(categoryLogger, e);
+ } else {
+ doLog(LOG, e);
+ }
+ }
+
+ /**
+ * Performs the actual logging.
+ *
+ * @param logger the provided logger to use.
+ * @param e the exception to log.
+ */
+ protected void doLog(Logger logger, Exception e) {
+ if (logLevel == null) {
+ logger.debug(e.getMessage(), e);
+ return;
+ }
+
+ if ("trace".equalsIgnoreCase(logLevel)) {
+ logger.trace(e.getMessage(), e);
+ } else if ("debug".equalsIgnoreCase(logLevel)) {
+ logger.debug(e.getMessage(), e);
+ } else if ("info".equalsIgnoreCase(logLevel)) {
+ logger.info(e.getMessage(), e);
+ } else if ("warn".equalsIgnoreCase(logLevel)) {
+ logger.warn(e.getMessage(), e);
+ } else if ("error".equalsIgnoreCase(logLevel)) {
+ logger.error(e.getMessage(), e);
+ } else if ("fatal".equalsIgnoreCase(logLevel)) {
+ logger.fatal(e.getMessage(), e);
+ } else {
+ throw new IllegalArgumentException("LogLevel [" + logLevel + "] is not supported");
+ }
+ }
+
+ /**
+ * @deprecated since 2.3.15 please use #findMappingFromExceptions directly instead
+ */
+ protected String findResultFromExceptions(List<ExceptionMappingConfig> exceptionMappings, Throwable t) {
+ ExceptionMappingConfig result = findMappingFromExceptions(exceptionMappings, t);
+ return result==null?null:result.getResult();
+ }
+
+ /**
+ * Try to find appropriate {@link ExceptionMappingConfig} based on provided Throwable
+ *
+ * @param exceptionMappings list of defined exception mappings
+ * @param t caught exception
+ * @return appropriate mapping or null
+ */
+ protected ExceptionMappingConfig findMappingFromExceptions(List<ExceptionMappingConfig> exceptionMappings, Throwable t) {
+ ExceptionMappingConfig config = null;
+ // Check for specific exception mappings.
+ if (exceptionMappings != null) {
+ int deepest = Integer.MAX_VALUE;
+ for (Object exceptionMapping : exceptionMappings) {
+ ExceptionMappingConfig exceptionMappingConfig = (ExceptionMappingConfig) exceptionMapping;
+ int depth = getDepth(exceptionMappingConfig.getExceptionClassName(), t);
+ if (depth >= 0 && depth < deepest) {
+ deepest = depth;
+ config = exceptionMappingConfig;
+ }
+ }
+ }
+ return config;
+ }
+
+ /**
+ * Return the depth to the superclass matching. 0 means ex matches exactly. Returns -1 if there's no match.
+ * Otherwise, returns depth. Lowest depth wins.
+ *
+ * @param exceptionMapping the mapping classname
+ * @param t the cause
+ * @return the depth, if not found -1 is returned.
+ */
+ public int getDepth(String exceptionMapping, Throwable t) {
+ return getDepth(exceptionMapping, t.getClass(), 0);
+ }
+
+ private int getDepth(String exceptionMapping, Class exceptionClass, int depth) {
+ if (exceptionClass.getName().contains(exceptionMapping)) {
+ // Found it!
+ return depth;
+ }
+ // If we've gone as far as we can go and haven't found it...
+ if (exceptionClass.equals(Throwable.class)) {
+ return -1;
+ }
+ return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
+ }
+
+ /**
+ * Default implementation to handle ExceptionHolder publishing. Pushes given ExceptionHolder on the stack.
+ * Subclasses may override this to customize publishing.
+ *
+ * @param invocation The invocation to publish Exception for.
+ * @param exceptionHolder The exceptionHolder wrapping the Exception to publish.
+ */
+ protected void publishException(ActionInvocation invocation, ExceptionHolder exceptionHolder) {
+ invocation.getStack().push(exceptionHolder);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/struts/blob/31af5842/core/src/main/java/com/opensymphony/xwork2/interceptor/I18nInterceptor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/I18nInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/I18nInterceptor.java
new file mode 100644
index 0000000..afdd534
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/I18nInterceptor.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2002-2006,2009 The Apache Software Foundation.
+ *
+ * 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 com.opensymphony.xwork2.interceptor;
+
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.util.LocalizedTextUtil;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * <!-- START SNIPPET: description -->
+ * <p/>
+ * An interceptor that handles setting the locale specified in a session as the locale for the current action request.
+ * In addition, this interceptor will look for a specific HTTP request parameter and set the locale to whatever value is
+ * provided. This means that this interceptor can be used to allow for your application to dynamically change the locale
+ * for the user's session or, alternatively, only for the current request (since XWork 2.1.3).
+ * This is very useful for applications that require multi-lingual support and want the user to
+ * be able to set his or her language preference at any point. The locale parameter is removed during the execution of
+ * this interceptor, ensuring that properties aren't set on an action (such as request_locale) that have no typical
+ * corresponding setter in your action.
+ * <p/>
+ * <p/>For example, using the default parameter name, a request to <b>foo.action?request_locale=en_US</b>, then the
+ * locale for US English is saved in the user's session and will be used for all future requests.
+ * <p/>
+ if there is no locale set (for example with the first visit), the interceptor uses the browser locale.
+ * <p/>
+ * <!-- END SNIPPET: description -->
+ * <p/>
+ * <p/> <u>Interceptor parameters:</u>
+ * <p/>
+ * <!-- START SNIPPET: parameters -->
+ * <p/>
+ * <ul>
+ * <p/>
+ * <li>parameterName (optional) - the name of the HTTP request parameter that dictates the locale to switch to and save
+ * in the session. By default this is <b>request_locale</b></li>
+ * <p/>
+ * <li>requestOnlyParameterName (optional) - the name of the HTTP request parameter that dictates the locale to switch to
+ * for the current request only, without saving it in the session. By default this is <b>request_only_locale</b></li>
+ * <p/>
+ * <li>attributeName (optional) - the name of the session key to store the selected locale. By default this is
+ * <b>WW_TRANS_I18N_LOCALE</b></li>
+ * <p/>
+ * </ul>
+ * <p/>
+ * <!-- END SNIPPET: parameters -->
+ * <p/>
+ * <p/> <u>Extending the interceptor:</u>
+ * <p/>
+ * <p/>
+ * <p/>
+ * <!-- START SNIPPET: extending -->
+ * <p/>
+ * There are no known extensions points for this interceptor.
+ * <p/>
+ * <!-- END SNIPPET: extending -->
+ * <p/>
+ * <p/> <u>Example code:</u>
+ * <p/>
+ * <pre>
+ * <!-- START SNIPPET: example -->
+ * <action name="someAction" class="com.examples.SomeAction">
+ * <interceptor-ref name="i18n"/>
+ * <interceptor-ref name="basicStack"/>
+ * <result name="success">good_result.ftl</result>
+ * </action>
+ * <!-- END SNIPPET: example -->
+ * </pre>
+ *
+ * @author Aleksei Gopachenko
+ */
+public class I18nInterceptor extends AbstractInterceptor {
+ private static final long serialVersionUID = 2496830135246700300L;
+
+ protected static final Logger LOG = LogManager.getLogger(I18nInterceptor.class);
+
+ public static final String DEFAULT_SESSION_ATTRIBUTE = "WW_TRANS_I18N_LOCALE";
+ public static final String DEFAULT_PARAMETER = "request_locale";
+ public static final String DEFAULT_REQUESTONLY_PARAMETER = "request_only_locale";
+
+ protected String parameterName = DEFAULT_PARAMETER;
+ protected String requestOnlyParameterName = DEFAULT_REQUESTONLY_PARAMETER;
+ protected String attributeName = DEFAULT_SESSION_ATTRIBUTE;
+
+ // Request-Only = None
+ protected enum Storage { SESSION, NONE }
+
+ public I18nInterceptor() {
+ LOG.debug("new I18nInterceptor()");
+ }
+
+ public void setParameterName(String parameterName) {
+ this.parameterName = parameterName;
+ }
+
+ public void setRequestOnlyParameterName(String requestOnlyParameterName) {
+ this.requestOnlyParameterName = requestOnlyParameterName;
+ }
+
+ public void setAttributeName(String attributeName) {
+ this.attributeName = attributeName;
+ }
+
+ @Override
+ public String intercept(ActionInvocation invocation) throws Exception {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Intercept '{}/{}' {", invocation.getProxy().getNamespace(), invocation.getProxy().getActionName());
+ }
+
+ LocaleFinder localeFinder = new LocaleFinder(invocation);
+ Locale locale = getLocaleFromParam(localeFinder.getRequestedLocale());
+ locale = storeLocale(invocation, locale, localeFinder.getStorage());
+ saveLocale(invocation, locale);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("before Locale: {}", invocation.getStack().findValue("locale"));
+ }
+
+ final String result = invocation.invoke();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("after Locale {}", invocation.getStack().findValue("locale"));
+ LOG.debug("intercept } ");
+ }
+
+ return result;
+ }
+
+ /**
+ * Store the locale to the chosen storage, like f. e. the session
+ *
+ * @param invocation the action invocation
+ * @param locale the locale to store
+ * @param storage the place to store this locale (like Storage.SESSSION.toString())
+ */
+ protected Locale storeLocale(ActionInvocation invocation, Locale locale, String storage) {
+ //save it in session
+ Map<String, Object> session = invocation.getInvocationContext().getSession();
+
+ if (session != null) {
+ synchronized (session) {
+ if (locale == null) {
+ storage = Storage.NONE.toString();
+ locale = readStoredLocale(invocation, session);
+ }
+
+ if (Storage.SESSION.toString().equals(storage)) {
+ session.put(attributeName, locale);
+ }
+ }
+ }
+ return locale;
+ }
+
+ protected class LocaleFinder {
+ protected String storage = Storage.SESSION.toString();
+ protected Object requestedLocale = null;
+
+ protected ActionInvocation actionInvocation = null;
+
+ protected LocaleFinder(ActionInvocation invocation) {
+ actionInvocation = invocation;
+ find();
+ }
+
+ protected void find() {
+ //get requested locale
+ Map<String, Object> params = actionInvocation.getInvocationContext().getParameters();
+
+ storage = Storage.SESSION.toString();
+
+ requestedLocale = findLocaleParameter(params, parameterName);
+ if (requestedLocale != null) {
+ return;
+ }
+
+ requestedLocale = findLocaleParameter(params, requestOnlyParameterName);
+ if (requestedLocale != null) {
+ storage = Storage.NONE.toString();
+ }
+ }
+
+ public String getStorage() {
+ return storage;
+ }
+
+ public Object getRequestedLocale() {
+ return requestedLocale;
+ }
+ }
+
+ /**
+ * Creates a Locale object from the request param, which might
+ * be already a Local or a String
+ *
+ * @param requestedLocale the parameter from the request
+ * @return the Locale
+ */
+ protected Locale getLocaleFromParam(Object requestedLocale) {
+ Locale locale = null;
+ if (requestedLocale != null) {
+ locale = (requestedLocale instanceof Locale) ?
+ (Locale) requestedLocale :
+ LocalizedTextUtil.localeFromString(requestedLocale.toString(), null);
+ if (locale != null) {
+ LOG.debug("Applied request locale: {}", locale);
+ }
+ }
+ return locale;
+ }
+
+ /**
+ * Reads the locale from the session, and if not found from the
+ * current invocation (=browser)
+ *
+ * @param invocation the current invocation
+ * @param session the current session
+ * @return the read locale
+ */
+ protected Locale readStoredLocale(ActionInvocation invocation, Map<String, Object> session) {
+ Locale locale = this.readStoredLocalFromSession(invocation, session);
+
+ if (locale != null) {
+ return locale;
+ }
+
+ return this.readStoredLocalFromCurrentInvocation(invocation);
+ }
+
+ protected Locale readStoredLocalFromSession(ActionInvocation invocation, Map<String, Object> session) {
+ // check session for saved locale
+ Object sessionLocale = session.get(attributeName);
+ if (sessionLocale != null && sessionLocale instanceof Locale) {
+ Locale locale = (Locale) sessionLocale;
+ LOG.debug("Applied session locale: {}", locale);
+ return locale;
+ }
+ return null;
+ }
+
+ protected Locale readStoredLocalFromCurrentInvocation(ActionInvocation invocation) {
+ // no overriding locale definition found, stay with current invocation (=browser) locale
+ Locale locale = invocation.getInvocationContext().getLocale();
+ if (locale != null) {
+ LOG.debug("Applied invocation context locale: {}", locale);
+ }
+ return locale;
+ }
+
+ protected Object findLocaleParameter(Map<String, Object> params, String parameterName) {
+ Object requestedLocale = params.remove(parameterName);
+ if (requestedLocale != null && requestedLocale.getClass().isArray()
+ && ((Object[]) requestedLocale).length > 0) {
+ requestedLocale = ((Object[]) requestedLocale)[0];
+
+ LOG.debug("Requested locale: {}", requestedLocale);
+ }
+ return requestedLocale;
+ }
+
+ /**
+ * Save the given locale to the ActionInvocation.
+ *
+ * @param invocation The ActionInvocation.
+ * @param locale The locale to save.
+ */
+ protected void saveLocale(ActionInvocation invocation, Locale locale) {
+ invocation.getInvocationContext().setLocale(locale);
+ }
+
+}