Fix issue11: create a Java instance with the following priority to choose the class: Explicit tag -> Runtime class (defined in JavaBean) -> implicit tag
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 452704b..67ad4cb 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -8,9 +8,8 @@
<body>
<release version="1.4-SNAPSHOT" date="in Mercurial" description="better support for immutable objects">
<action dev="py4fun" type="fix" issue="11" due-to="infinity0x">
- Fix: in case there is more then 1 constructor with 1 argument to create an instance
- for scalar node and a global or local tag does not allow to rely on the implicit type
- then try to use the constructor with the String argument (2009-08-05)
+ Fix: create a Java instance with the following priority to choose the class:
+ Explicit tag -> Runtime class (defined in JavaBean) -> implicit tag (2009-08-06)
</action>
<action dev="py4fun" type="fix" issue="9" due-to="wwagner4">
Fix: Bean with no property cannot be instantiated. This is implemented via better
diff --git a/src/main/java/org/yaml/snakeyaml/composer/Composer.java b/src/main/java/org/yaml/snakeyaml/composer/Composer.java
index a9ad0b4..bbd0ea9 100644
--- a/src/main/java/org/yaml/snakeyaml/composer/Composer.java
+++ b/src/main/java/org/yaml/snakeyaml/composer/Composer.java
@@ -133,11 +133,13 @@
private Node composeScalarNode(String anchor) {
ScalarEvent ev = (ScalarEvent) parser.getEvent();
String tag = ev.getTag();
+ boolean explicitTag = true;
if (tag == null || tag.equals("!")) {
tag = resolver.resolve(NodeId.scalar, ev.getValue(), ev.getImplicit()[0]);
+ explicitTag = false;
}
- Node node = new ScalarNode(tag, ev.getValue(), ev.getStartMark(), ev.getEndMark(), ev
- .getStyle());
+ Node node = new ScalarNode(tag, explicitTag, ev.getValue(), ev.getStartMark(), ev
+ .getEndMark(), ev.getStyle());
if (anchor != null) {
anchors.put(anchor, node);
}
diff --git a/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java b/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java
index 686f269..7d8e672 100644
--- a/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java
+++ b/src/main/java/org/yaml/snakeyaml/constructor/BaseConstructor.java
@@ -30,13 +30,16 @@
public abstract class BaseConstructor {
/**
* It maps the node kind to the the Construct implementation. When the
- * runtime class is known then the tag (even explicit) is ignored.
+ * runtime class is known then the implicit tag is ignored.
*/
- protected final Map<NodeId, Construct> yamlClassConstructors = new EnumMap<NodeId, Construct>(NodeId.class);
+ protected final Map<NodeId, Construct> yamlClassConstructors = new EnumMap<NodeId, Construct>(
+ NodeId.class);
/**
- * It maps a the resolved tag to the Construct implementation. It is used
- * when the runtime class of the instance is unknown (the node has the
- * Object.class)
+ * It maps the (explicit or implicit) tag to the Construct implementation.
+ * It is used: <br/>
+ * 1) explicit tag - if present. <br/>
+ * 2) implicit tag - when the runtime class of the instance is unknown (the
+ * node has the Object.class)
*/
protected final Map<String, Construct> yamlConstructors = new HashMap<String, Construct>();
@@ -177,16 +180,17 @@
}
/**
- * Get the constructor to construct the Node. If the runtime class is known
- * a dedicated Construct implementation is used. Otherwise the constructor
- * is chosen by the tag.
+ * Get the constructor to construct the Node. For implicit tags if the
+ * runtime class is known a dedicated Construct implementation is used.
+ * Otherwise the constructor is chosen by the tag.
*
* @param node
* Node to be constructed
* @return Construct implementation for the specified node
*/
private Construct getConstructor(Node node) {
- if (!Object.class.equals(node.getType()) && !node.getTag().equals(Tags.NULL)) {
+ if (!node.hasExplicitTag() && !Object.class.equals(node.getType())
+ && !node.getTag().equals(Tags.NULL)) {
return yamlClassConstructors.get(node.getNodeId());
} else {
Construct constructor = yamlConstructors.get(node.getTag());
diff --git a/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java b/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java
index b7d5719..64d30b4 100644
--- a/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java
+++ b/src/main/java/org/yaml/snakeyaml/constructor/Constructor.java
@@ -305,10 +305,6 @@
result = constructStandardJavaInstance(type, node);
} else {
// there must be only 1 constructor with 1 argument
- if (Modifier.isAbstract(type.getModifiers())) {
- // use the tag when the runtime class cannot be instantiated
- type = getClassForNode(node);
- }
java.lang.reflect.Constructor[] javaConstructors = type.getConstructors();
int oneArgCount = 0;
java.lang.reflect.Constructor javaConstructor = null;
@@ -328,7 +324,9 @@
// TODO it should be possible to use implicit types instead
// of forcing String. Resolver must be available here to
// obtain the implicit tag. Then we can set the tag and call
- // callConstructor(node) to create the argument instance
+ // callConstructor(node) to create the argument instance.
+ // On the other hand it may be safer to require a custom
+ // constructor to avoid guessing the argument class
argument = constructScalar(node);
try {
javaConstructor = type.getConstructor(String.class);
@@ -417,9 +415,6 @@
throw new YAMLException("Unable to find enum value '" + enumValueName
+ "' for enum class: " + type.getName());
}
- } else if (Tags.BINARY.equals(node.getTag())) {
- Construct intConstructor = yamlConstructors.get(Tags.BINARY);
- result = intConstructor.construct(node);
} else {
throw new YAMLException("Unsupported class: " + type);
}
diff --git a/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java b/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java
index 19d5dc4..fb370f5 100644
--- a/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java
+++ b/src/main/java/org/yaml/snakeyaml/events/CollectionStartEvent.java
@@ -10,7 +10,10 @@
*/
public abstract class CollectionStartEvent extends NodeEvent {
private final String tag;
+ // The implicit flag of a collection start event indicates if the tag may be
+ // omitted when the collection is emitted
private final boolean implicit;
+ // flag indicates if a collection is block or flow
private final Boolean flowStyle;
public CollectionStartEvent(String anchor, String tag, boolean implicit, Mark startMark,
diff --git a/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java b/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java
index b76d9b7..c8a1917 100644
--- a/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java
+++ b/src/main/java/org/yaml/snakeyaml/events/ScalarEvent.java
@@ -10,8 +10,14 @@
*/
public final class ScalarEvent extends NodeEvent {
private final String tag;
+ // style flag of a scalar event indicates the style of the scalar. Possible
+ // values are None, '', '\'', '"', '|', '>'
private final Character style;
private final String value;
+ // The implicit flag of a scalar event is a pair of boolean values that
+ // indicate if the tag may be omitted when the scalar is emitted in a plain
+ // and non-plain style correspondingly.
+ //TODO should we use tuple ?
private final boolean[] implicit;
public ScalarEvent(String anchor, String tag, boolean[] implicit, String value, Mark startMark,
diff --git a/src/main/java/org/yaml/snakeyaml/nodes/Node.java b/src/main/java/org/yaml/snakeyaml/nodes/Node.java
index 5a7dfac..abd33fc 100644
--- a/src/main/java/org/yaml/snakeyaml/nodes/Node.java
+++ b/src/main/java/org/yaml/snakeyaml/nodes/Node.java
@@ -14,6 +14,11 @@
protected Mark endMark;
private Class<? extends Object> type;
private boolean twoStepsConstruction;
+ // Plain scalars without explicitly defined tag are subject to implicit tag
+ // resolution. The scalar value is checked against a set of regular
+ // expressions.
+ // This is false when the tag was resolved
+ protected boolean explicitTag;
public Node(String tag, Mark startMark, Mark endMark) {
setTag(tag);
@@ -21,6 +26,7 @@
this.endMark = endMark;
this.type = Object.class;
this.twoStepsConstruction = false;
+ this.explicitTag = false;
}
public String getTag() {
@@ -78,4 +84,13 @@
public final int hashCode() {
return super.hashCode();
}
+
+ /**
+ * Check if the tag is defined in the YAML document
+ *
+ * @return true when the tag is explicit, false when the tag is resolved
+ */
+ public boolean hasExplicitTag() {
+ return explicitTag;
+ }
}
diff --git a/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java b/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java
index 191c7d7..d669db1 100644
--- a/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java
+++ b/src/main/java/org/yaml/snakeyaml/nodes/ScalarNode.java
@@ -13,12 +13,18 @@
private String value;
public ScalarNode(String tag, String value, Mark startMark, Mark endMark, Character style) {
+ this(tag, false, value, startMark, endMark, style);
+ }
+
+ public ScalarNode(String tag, boolean explicit, String value, Mark startMark, Mark endMark,
+ Character style) {
super(tag, startMark, endMark);
if (value == null) {
throw new NullPointerException("value in a Node is required.");
}
this.value = value;
this.style = style;
+ this.explicitTag = explicit;
}
public Character getStyle() {
diff --git a/src/test/java/org/yaml/snakeyaml/JavaBeanWithNullValuesTest.java b/src/test/java/org/yaml/snakeyaml/JavaBeanWithNullValuesTest.java
index 30948b4..2f569ee 100644
--- a/src/test/java/org/yaml/snakeyaml/JavaBeanWithNullValuesTest.java
+++ b/src/test/java/org/yaml/snakeyaml/JavaBeanWithNullValuesTest.java
@@ -8,7 +8,10 @@
import junit.framework.TestCase;
+import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tags;
+import org.yaml.snakeyaml.representer.Represent;
+import org.yaml.snakeyaml.representer.Representer;
public class JavaBeanWithNullValuesTest extends TestCase {
private JavaBeanLoader<JavaBeanWithNullValues> loader;
@@ -20,6 +23,7 @@
public void testNotNull() throws Exception {
String dumpStr = dumpJavaBeanWithNullValues(false);
+ // System.out.println(dumpStr);
Yaml yaml = new Yaml();
JavaBeanWithNullValues parsed = (JavaBeanWithNullValues) yaml.load(dumpStr);
assertNotNull(parsed.getString());
@@ -32,7 +36,6 @@
assertNotNull(parsed.getSqlDate());
assertNotNull(parsed.getTimestamp());
//
-
parsed = loader.load(dumpStr);
assertNotNull(parsed.getString());
assertNotNull(parsed.getBoolean1());
@@ -61,7 +64,7 @@
options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED);
options.setExplicitStart(true);
options.setExplicitEnd(true);
- Yaml yaml = new Yaml(options);
+ Yaml yaml = new Yaml(new Dumper(new CustomRepresenter(), options));
javaBeanWithNullValues.setBoolean1(null);
javaBeanWithNullValues.setDate(new Date(System.currentTimeMillis()));
javaBeanWithNullValues.setDouble1(1d);
@@ -73,6 +76,7 @@
javaBeanWithNullValues.setTimestamp(new Timestamp(System.currentTimeMillis()));
String dumpStr = yaml.dump(javaBeanWithNullValues);
+ // System.out.println(dumpStr);
yaml = new Yaml();
JavaBeanWithNullValues parsed = (JavaBeanWithNullValues) yaml.load(dumpStr);
assertNull(" expect null, got " + parsed.getBoolean1(), parsed.getBoolean1());
@@ -86,7 +90,7 @@
options.setExplicitStart(true);
options.setExplicitEnd(true);
options.setExplicitRoot(Tags.MAP);
- Yaml yaml = new Yaml(options);
+ Yaml yaml = new Yaml(new Dumper(new CustomRepresenter(), options));
javaBeanWithNullValues.setBoolean1(null);
javaBeanWithNullValues.setDate(new Date(System.currentTimeMillis()));
javaBeanWithNullValues.setDouble1(1d);
@@ -101,7 +105,7 @@
// System.out.println(dumpStr);
assertFalse("No explicit root tag must be used.", dumpStr
.contains("JavaBeanWithNullValues"));
- yaml = new Yaml();
+ yaml = new Yaml(new Dumper(new CustomRepresenter(), options));
JavaBeanWithNullValues parsed = loader.load(dumpStr);
assertNull(" expect null, got " + parsed.getBoolean1(), parsed.getBoolean1());
assertNull(" expect null, got " + parsed.getString(), parsed.getString());
@@ -117,7 +121,7 @@
options.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED);
options.setExplicitStart(true);
options.setExplicitEnd(true);
- Yaml yaml = new Yaml(options);
+ Yaml yaml = new Yaml(new Dumper(new CustomRepresenter(), options));
if (nullValues) {
return yaml.dump(javaBeanWithNullValues);
}
@@ -133,4 +137,38 @@
return yaml.dump(javaBeanWithNullValues);
}
+ public class CustomRepresenter extends Representer {
+ public CustomRepresenter() {
+ this.representers.put(Float.class, new RepresentFloat());
+ this.representers.put(Long.class, new RepresentLong());
+ this.representers.put(java.sql.Date.class, new RepresentDate());
+ this.representers.put(java.sql.Timestamp.class, new RepresentTime());
+ }
+
+ private class RepresentFloat implements Represent {
+ public Node representData(Object data) {
+ return representScalar(Tags.PREFIX + "java.lang.Float", ((Float) data).toString());
+ }
+ }
+
+ private class RepresentLong implements Represent {
+ public Node representData(Object data) {
+ return representScalar(Tags.PREFIX + "java.lang.Long", ((Long) data).toString());
+ }
+ }
+
+ private class RepresentDate implements Represent {
+ public Node representData(Object data) {
+ return representScalar(Tags.PREFIX + "java.sql.Date", ((java.sql.Date) data)
+ .toString());
+ }
+ }
+
+ private class RepresentTime implements Represent {
+ public Node representData(Object data) {
+ return representScalar(Tags.PREFIX + "java.sql.Timestamp",
+ ((java.sql.Timestamp) data).toString());
+ }
+ }
+ }
}
diff --git a/src/test/java/org/yaml/snakeyaml/issues/issue11/YamlMapTest.java b/src/test/java/org/yaml/snakeyaml/issues/issue11/YamlMapTest.java
index 0442d2f..fc734e2 100644
--- a/src/test/java/org/yaml/snakeyaml/issues/issue11/YamlMapTest.java
+++ b/src/test/java/org/yaml/snakeyaml/issues/issue11/YamlMapTest.java
@@ -21,12 +21,19 @@
import org.yaml.snakeyaml.representer.Representer;
public class YamlMapTest extends TestCase {
+ public void testYaml() throws IOException {
+ Yaml yaml = new Yaml(new Loader(new ExtendedConstructor()), new Dumper(
+ new ExtendedRepresenter(), new DumperOptions()));
+ String output = yaml.dump(new Custom(123));
+ // System.out.println(output);
+ Custom o = (Custom) yaml.load(output);
+ assertEquals("123", o.getStr());
+ }
@SuppressWarnings("unchecked")
public void testYamlMap() throws IOException {
Map<String, Object> data = new TreeMap<String, Object>();
- data.put("key3", new Custom("test"));
- data.put("key4", new Wrapper("test", new Custom("test")));
+ data.put("customTag", new Custom(123));
Yaml yaml = new Yaml(new Loader(new ExtendedConstructor()), new Dumper(
new ExtendedRepresenter(), new DumperOptions()));
@@ -36,8 +43,23 @@
assertTrue(o instanceof Map);
Map<String, Object> m = (Map<String, Object>) o;
- assertTrue(m.get("key3") instanceof Custom);
- assertTrue(m.get("key4") instanceof Wrapper);
+ assertTrue(m.get("customTag") instanceof Custom);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testYamlMapBean() throws IOException {
+ Map<String, Object> data = new TreeMap<String, Object>();
+ data.put("knownClass", new Wrapper("test", new Custom(456)));
+
+ Yaml yaml = new Yaml(new Loader(new ExtendedConstructor()), new Dumper(
+ new ExtendedRepresenter(), new DumperOptions()));
+ String output = yaml.dump(data);
+ // System.out.println(output);
+ Object o = yaml.load(output);
+
+ assertTrue(o instanceof Map);
+ Map<String, Object> m = (Map<String, Object>) o;
+ assertEquals(Wrapper.class, m.get("knownClass").getClass());
}
public static class Wrapper {
@@ -72,12 +94,8 @@
public static class Custom {
final private String str;
- public Custom(String s) {
- str = s;
- }
-
public Custom(Integer i) {
- str = "";
+ str = i.toString();
}
public Custom(Custom c) {
@@ -87,6 +105,10 @@
public String toString() {
return str;
}
+
+ public String getStr() {
+ return str;
+ }
}
public static class ExtendedRepresenter extends Representer {
@@ -109,7 +131,7 @@
private class ConstructCustom extends AbstractConstruct {
public Object construct(Node node) {
String str = (String) constructScalar((ScalarNode) node);
- return new Custom(str);
+ return new Custom(Integer.parseInt(str));
}
}
diff --git a/src/test/java/org/yaml/snakeyaml/issues/issue9/BeanConstructor.java b/src/test/java/org/yaml/snakeyaml/issues/issue9/BeanConstructor.java
index beb53e0..b77a89f 100644
--- a/src/test/java/org/yaml/snakeyaml/issues/issue9/BeanConstructor.java
+++ b/src/test/java/org/yaml/snakeyaml/issues/issue9/BeanConstructor.java
@@ -4,36 +4,38 @@
package org.yaml.snakeyaml.issues.issue9;
import org.yaml.snakeyaml.constructor.Constructor;
-import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.nodes.Node;
-import org.yaml.snakeyaml.nodes.NodeId;
import org.yaml.snakeyaml.nodes.ScalarNode;
public class BeanConstructor extends Constructor {
public BeanConstructor() {
super(BeanHolder.class);
- yamlClassConstructors.put(NodeId.scalar, new BeanScalarConstructor());
+
+ // TODO the tag may start with !!
+ // yamlConstructors.put("tag:yaml.org,2002:org.yaml.snakeyaml.issues.issue9.Bean1",
+ yamlConstructors.put("tag:yaml.org,2002:org.yaml.snakeyaml.issues.issue9.Bean1",
+ new Bean1ScalarConstructor());
+ yamlConstructors.put("tag:yaml.org,2002:org.yaml.snakeyaml.issues.issue9.BeanHolder",
+ new BeanHolderScalarConstructor());
}
- private class BeanScalarConstructor extends ConstructScalar {
+ private class Bean1ScalarConstructor extends ConstructScalar {
@Override
public Object construct(Node node) {
ScalarNode snode = (ScalarNode) node;
- if (BeanHolder.class.equals(node.getType()) || IBean.class.equals(node.getType())) {
- if (snode.getValue().length() == 0) {
- try {
- return getClassForNode(node).newInstance();
- } catch (Exception e) {
- throw new YAMLException("BeanHolder cannot be created");
- }
- } else {
- throw new YAMLException("BeanHolder cannot be created out of '"
- + snode.getValue() + "'");
- }
+ if (snode.getValue().length() == 0) {
+ return new Bean1();
} else {
- return super.construct(node);
+ return new Bean1(Integer.parseInt(snode.getValue()));
}
}
}
+
+ private class BeanHolderScalarConstructor extends ConstructScalar {
+ @Override
+ public Object construct(Node node) {
+ return new BeanHolder();
+ }
+ }
}
diff --git a/src/test/java/org/yaml/snakeyaml/javabeans/TriangleBeanTest.java b/src/test/java/org/yaml/snakeyaml/javabeans/TriangleBeanTest.java
index 20a8377..462a8be 100644
--- a/src/test/java/org/yaml/snakeyaml/javabeans/TriangleBeanTest.java
+++ b/src/test/java/org/yaml/snakeyaml/javabeans/TriangleBeanTest.java
@@ -44,16 +44,19 @@
}
/**
- * Runtime class is more important then the tag (which is ignored if the
- * class is known)
+ * Runtime class has less priority then an explicit tag
*/
public void testClassAndTag() {
String output = "name: !!whatever Bean25\nshape: !!org.yaml.snakeyaml.javabeans.Triangle\n name: Triangle25\n";
JavaBeanLoader<TriangleBean> beanLoader = new JavaBeanLoader<TriangleBean>(
TriangleBean.class);
- TriangleBean loadedBean = beanLoader.load(output);
- assertNotNull(loadedBean);
- assertEquals("Bean25", loadedBean.getName());
- assertEquals(7, loadedBean.getShape().process());
+ try {
+ beanLoader.load(output);
+ fail("Runtime class has less priority then an explicit tag");
+ } catch (Exception e) {
+ assertEquals(
+ "Cannot create property=name for JavaBean=TriangleBean name=null; null; Can't construct a java object for tag:yaml.org,2002:whatever; exception=Class not found: whatever",
+ e.getMessage());
+ }
}
}