Search code examples
javaxml-parsingapache-commons-digester

How can I call a method with popped object using commons-digester?


I have an XML document that looks like this:

<!-- language: xml -->
<items>
  <item type="java.lang.Boolean" name="foo" value="true" />
</items>

I'd like the <root> element to create a java.util.Map object and have each <item> element create an object of the appropriate type and then add an entry to the Map -- similar to a SetNextRule but with an argument to the call coming from the stack.

I've already created a custom Rule that will create an object of the type specified in the type attribute (java.lang.Boolean in this case) using the value in the value attribute and push it on the stack.

Now, I'd like to pop the item off the top of the stack and use it as an argument to the put method on the Map object (which is just "under" the Boolean object on the stack).

Here's the code I have written so far:

<!-- language: lang-java -->
Digester digester = new Digester();
digester.addObjectCreate("items", HashMap.class);
digester.addRule(new MyObjectCreateRule()); // This knows how to create e.g. java.lang.Boolean objects
digester.addCallMethod("items/item", "put", 2, new Class<?>[] { String.class, Object.class });
digester.addCallParam("items/item", 0, "name");
digester.addCallParam("items/item", 1, true); // take argument from stack

I'm getting the error that the method put can't be found in the java.lang.Boolean class. So, the problem is that the e.g. Boolean object is on the top of the stack, and I want to use it as an argument to the put method called on the next-to-top element on the stack:

Stack:

java.lang.Boolean value=true     <-- top of stack, desired call param
java.util.HashMap contents = {}  <-- desired call target

Is there a way to do this with existing commons-digester rules, or do I have to create another custom rule that performs this type of operation?


Solution

  • I ended up writing a custom rule that combined the two operations: constructing a new instance of the property value and inserting it into the properties bundle.

    This is an adaptation of the real use-case that I had, so the code may not be 100% perfect since I copy/pasted and adapted it, here. I also understand that using property values other than java.lang.String doesn't quite make sense, but it did for my use-case (which doesn't use java.util.Properties, actually, but that class was a good analogy).

    <!-- language: lang-java -->
    /**
     * Implements a create-object-set-property Digester rule.
     */
    public class SetPropertyRule
        extends Rule
    {
        private String _classAttributeName;
        private String _nameAttributeName;
        private String _valueAttributeName;
        private HashSet<String> _acceptableClassNames;
    
        /**
         * Creates a new SetPreferenceRule with default attribute names and classes.
         *
         * Default class attribute name = "type".
         * Default name attribute name = "name".
         * Default value attribute name = "value".
         * Default allowed classes = String, Integer, Double, and Boolean.
         */
        public SetPropertiesRule()
        {
            this("type", "name", "value",
                 new Class<?>[] { String.class, Integer.class, Double.class, Boolean.class });
        }
    
        /**
         * Creates a new SetPropertyRule to construct a name/value pair and
         * set it on a Properties object.
         *
         * The Properties object should be at the top of the current
         * Digester stack.
         *
         * @param classAttributeName The name of the attribute that holds the property's value type.
         * @param nameAttributeName The name of the attribute that holds the property's name.
         * @param valueAttributeName The name of the attribute that holds the property's value.
         * @param acceptableClasses The list of acceptable property value types.
         */
        public SetPreferenceRule(String classAttributeName, String nameAttributeName, String valueAttributeName, Class<?>[] acceptableClasses)
        {
            super();
    
            _classAttributeName = classAttributeName;
            _nameAttributeName = nameAttributeName;
            _valueAttributeName = valueAttributeName;
            _acceptableClassNames = new HashSet<String>(acceptableClasses.length);
            for(Class<?> clazz : acceptableClasses)
                _acceptableClassNames.add(clazz.getName());
        }
    
        @Override
        public void begin(String namespace,
                          String name,
                          Attributes attributes)
            throws Exception
        {
            // Store the values of these attributes on the digester param stack
            getDigester().pushParams(
                    attributes.getValue(_classAttributeName),
                    attributes.getValue(_nameAttributeName),
                    attributes.getValue(_valueAttributeName)
            );
        }
    
        @Override
        public void end(String namespace,
                        String name)
            throws Exception
        {
            Object[] attributeValues = getDigester().popParams();
    
            Object props = getDigester().peek();
            if(!(props instanceof java.util.Properties))
            {
                String typeName;
                if(null == props)
                    typeName = "<null>";
                else
                    typeName = props.getClass().getName();
    
                throw new IllegalStateException("Expected instance of " + Properties.class.getName() + ", got " + typeName + " instead");
            }
    
            String className = (String)attributeValues[0];
            checkClassName(className);
    
            // Create an instance of the preference value class
            Class<?> clazz = Class.forName(className);
            Constructor<?> cons = clazz.getConstructor(String.class);
            Object value = cons.newInstance((String)attributeValues[2]);
    
            ((Properties)props).put((String)attributeValues[1], value);
        }
    
        private void checkClassName(String className)
        {
            if(!_acceptableClassNames.contains(className))
                throw new IllegalArgumentException("Class " + className + " is not allowed");
        }
    }
    

    I'd be happy to discover that there is an out-of-the-box way to do this, however.