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 -->
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;!-- The value for the foo parameter will be applied as if it were named bar --&gt;
+ *     &lt;param name="aliases"&gt;#{ 'foo' : 'bar' }&lt;/param&gt;
+ *
+ *     &lt;interceptor-ref name="alias"/&gt;
+ *     &lt;interceptor-ref name="basicStack"/&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ * <!-- 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>
+ * &lt;constant name="struts.xwork.chaining.copyErrors" value="true"/&gt;
+ * </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/>
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="basicStack"/&gt;
+ *     &lt;result name="success" type="chain"&gt;otherAction&lt;/result&gt;
+ * &lt;/action&gt;
+ * <p/>
+ * &lt;action name="otherAction" class="com.examples.OtherAction"&gt;
+ *     &lt;interceptor-ref name="chain"/&gt;
+ *     &lt;interceptor-ref name="basicStack"/&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ * <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 -->
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="params"/&gt;
+ *     &lt;interceptor-ref name="conversionError"/&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ * <!-- 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 -->
+ *
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="params"/&gt;
+ *     &lt;interceptor-ref name="validation"/&gt;
+ *     &lt;interceptor-ref name="workflow"/&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ *
+ * &lt;-- In this case myMethod as well as mySecondMethod of the action class
+ *        will not pass through the workflow process --&gt;
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="params"/&gt;
+ *     &lt;interceptor-ref name="validation"/&gt;
+ *     &lt;interceptor-ref name="workflow"&gt;
+ *         &lt;param name="excludeMethods"&gt;myMethod,mySecondMethod&lt;/param&gt;
+ *     &lt;/interceptor-ref name="workflow"&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ *
+ * &lt;-- In this case, the result named "error" will be used when
+ *        an action / field error is found --&gt;
+ * &lt;-- The Interceptor will only be applied for myWorkflowMethod method of action
+ *        classes, since this is the only included method while any others are excluded --&gt;
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="params"/&gt;
+ *     &lt;interceptor-ref name="validation"/&gt;
+ *     &lt;interceptor-ref name="workflow"&gt;
+ *        &lt;param name="inputResultName"&gt;error&lt;/param&gt;
+ *         &lt;param name="excludeMethods"&gt;*&lt;/param&gt;
+ *         &lt;param name="includeMethods"&gt;myWorkflowMethod&lt;/param&gt;
+ *     &lt;/interceptor-ref&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ *
+ * <!-- 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 -->
+ * &lt;xwork&gt;
+ *     &lt;package name="default" extends="xwork-default"&gt;
+ *         &lt;global-results&gt;
+ *             &lt;result name="error" type="freemarker"&gt;error.ftl&lt;/result&gt;
+ *         &lt;/global-results&gt;
+ *
+ *         &lt;global-exception-mappings&gt;
+ *             &lt;exception-mapping exception="java.lang.Exception" result="error"/&gt;
+ *         &lt;/global-exception-mappings&gt;
+ *
+ *         &lt;action name="test"&gt;
+ *             &lt;interceptor-ref name="exception"/&gt;
+ *             &lt;interceptor-ref name="basicStack"/&gt;
+ *             &lt;exception-mapping exception="com.acme.CustomException" result="custom_error"/&gt;
+ *             &lt;result name="custom_error"&gt;custom_error.ftl&lt;/result&gt;
+ *             &lt;result name="success" type="freemarker"&gt;test.ftl&lt;/result&gt;
+ *         &lt;/action&gt;
+ *     &lt;/package&gt;
+ * &lt;/xwork&gt;
+ * <!-- 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 -->
+ * &lt;xwork&gt;
+ *   &lt;package name="something" extends="xwork-default"&gt;
+ *      &lt;interceptors&gt;
+ *          &lt;interceptor-stack name="exceptionmappingStack"&gt;
+ *              &lt;interceptor-ref name="exception"&gt;
+ *                  &lt;param name="logEnabled"&gt;true&lt;/param&gt;
+ *                  &lt;param name="logCategory"&gt;com.mycompany.app.unhandled&lt;/param&gt;
+ *                  &lt;param name="logLevel"&gt;WARN&lt;/param&gt;	        		
+ *              &lt;/interceptor-ref&gt;	
+ *              &lt;interceptor-ref name="i18n"/&gt;
+ *              &lt;interceptor-ref name="staticParams"/&gt;
+ *              &lt;interceptor-ref name="params"/&gt;
+ *              &lt;interceptor-ref name="validation"&gt;
+ *                  &lt;param name="excludeMethods"&gt;input,back,cancel,browse&lt;/param&gt;
+ *              &lt;/interceptor-ref&gt;
+ *          &lt;/interceptor-stack&gt;
+ *      &lt;/interceptors&gt;
+ *
+ *      &lt;default-interceptor-ref name="exceptionmappingStack"/&gt;
+ *    
+ *      &lt;global-results&gt;
+ *           &lt;result name="unhandledException"&gt;/unhandled-exception.jsp&lt;/result&gt;
+ *      &lt;/global-results&gt;
+ *
+ *      &lt;global-exception-mappings&gt;
+ *           &lt;exception-mapping exception="java.lang.Exception" result="unhandledException"/&gt;
+ *      &lt;/global-exception-mappings&gt;
+ *        
+ *      &lt;action name="exceptionDemo" class="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingAction"&gt;
+ *          &lt;exception-mapping exception="org.apache.struts2.showcase.exceptionmapping.ExceptionMappingException"
+ *                             result="damm"/&gt;
+ *          &lt;result name="input"&gt;index.jsp&lt;/result&gt;
+ *          &lt;result name="success"&gt;success.jsp&lt;/result&gt;            
+ *          &lt;result name="damm"&gt;damm.jsp&lt;/result&gt;
+ *      &lt;/action&gt;
+ *
+ *   &lt;/package&gt;
+ * &lt;/xwork&gt;
+ * <!-- 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 -->
+ * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
+ *     &lt;interceptor-ref name="i18n"/&gt;
+ *     &lt;interceptor-ref name="basicStack"/&gt;
+ *     &lt;result name="success"&gt;good_result.ftl&lt;/result&gt;
+ * &lt;/action&gt;
+ * <!-- 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);
+    }
+
+}