Reputation: 20862
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
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
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