Christopher Schultz
Christopher Schultz

Reputation: 20862

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?

Upvotes: 2

Views: 498

Answers (2)

Barney
Barney

Reputation: 2836

For a different approach, you could move the problem out of digester itself and use an enhanced map class to provide a method more compatible with the existing digester rules:

public static class MyHashMap extends HashMap {
  public Object put(String clazz, String name, String value) {
    Object obj = ... // create object from clazz/name/value
    return super.put(name, obj);
  }
}

Then just use the existing addCallMethod / addCallParam rules:

Digester digester = new Digester();
digester.addObjectCreate("items", MyHashMap.class);
digester.addCallMethod("items/item", "put", 3, new Class<?>[] { String.class, String.class, String.class });
digester.addCallParam("items/item", 0, "type");
digester.addCallParam("items/item", 1, "name");
digester.addCallParam("items/item", 2, "value");

If you needed to get a pure HashMap as a result rather than a custom class, you could use a similar method with your custom class wrapping a native HashMap, e.g.com.google.common.collect.ForwardingMap if you are using Guava.

Upvotes: 0

Christopher Schultz
Christopher Schultz

Reputation: 20862

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.

Upvotes: 0

Related Questions