Custom implicit resolver does not applies inside JavaBean declaration

165 views
Skip to first unread message

Artem

unread,
Jan 7, 2010, 11:50:38 AM1/7/10
to SnakeYAML
Hi.
I trying to implement insertion of external properties to YAML
document.
It is a very simple with using custom tags - we need to implement a
Construct interface and treat string node with specific tag as
property key. Then fetch value of this key and return(in construct
(Node) method).

It looks like a following:

!testbean {myval: !cfg $(user.home)}

Then i tried to remove requirement of !cfg tag by adding implicit
resolver with regexp "\$\([a-zA-Z\d\u002E\u005F]+\)"
But for document "!testbean {myval: $(user.home)}" my constructor was
not called.
It seems what SnakeYAML always use type of the bean property if tag
does not specified.
I think what implicit application specific constructor must have
higher priority than native bean property constructor.

What do you think? May it be a bug or that restriction is justified?

Code to check:

package test;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.regex.Pattern;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.SystemConfiguration;
import org.yaml.snakeyaml.Loader;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.AbstractConstruct;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.ScalarNode;

public class ImplicitResolver {

public static class ConfigurationConstructor extends Constructor {
protected Configuration config = null;
public ConfigurationConstructor(Configuration config) {
this.config = config;
this.yamlConstructors.put("!cfg", new ConfigObjectConstruct
());
}

private class ConfigObjectConstruct extends AbstractConstruct {
public Object construct(Node node) {
String val = (String) constructScalar((ScalarNode) node);
val = val.substring(2, val.length()-1);
return config.getProperty(val);
}
}

}

public static class TestBean
{
String myval;

public String getMyval() {
return myval;
}

public void setMyval(String myval) {
this.myval = myval;
}

public String toString()
{
return "MyVal: "+myval;
}
}

public static void main(String args[])
{
Configuration config = new SystemConfiguration();
Constructor constructor = new ConfigurationConstructor(config);
constructor.addTypeDescription(new TypeDescription(
TestBean.class, "!testbean"));
Loader loader = new Loader(constructor);
Yaml yaml = new Yaml(loader);
yaml.addImplicitResolver("!cfg", Pattern.compile("\\$\\([a-zA-Z\\d\
\u002E\\u005F]+\\)"),
null);
System.out.println("Explicit");
Object bean = yaml.load("!testbean {myval: !cfg $(user.home)}");
System.out.println(bean.toString());
bean = yaml.load("!testbean {myval: $(user.home)}");
System.out.println("Implicit");
System.out.println(bean.toString());
}
}

P.S. I make some dirty hack to solve that problem but it can be fixed
in more correct way.

--- a/src/main/java/org/yaml/snakeyaml/composer/Composer.java Mon
Jan 04 19:55:07 2010 +0100
+++ b/src/main/java/org/yaml/snakeyaml/composer/Composer.java Thu
Jan 07 19:49:09 2010 +0300
@@ -176,6 +176,10 @@
}
Node node = new ScalarNode(tag, resolved, ev.getValue(),
ev.getStartMark(),
ev.getEndMark(), ev.getStyle());
+
+ if(!tag.startsWith("tag:yaml.org"))
+ node.setUseClassConstructor(false);
+
if (anchor != null) {
anchors.put(anchor, node);
}

Andrey

unread,
Jan 8, 2010, 4:50:14 AM1/8/10
to SnakeYAML
Hi Artem,
thank you very much for your time.
This is an important issue. You are right this is a priority question.

Current priority:
1) explicit tag has the highest priority. Because the meaning of a tag
can be specified at runtime it gives a lot of flexibility.
Unfortunately Java has more then 1 implementation for standard tags.
(float -> Float, Double, BigDecimal) That is why if the tag is
standard and it is compatible with the property class of the JavaBean
the tag is ignored. See the issue 40 for details.

2) when the class is known (JavaBean properties) it is used

3) implicit tag is used when no other runtime information is available

SnakeYAML is used in different contexts. Often a YAML document comes
from another party and developers cannot play with tags there. In such
a case developers want to have more control to manage tags (or even to
completely ignore them!).
In your case it is the other way around.

I must say I do not have a clean solution right away. I will think how
we can get more flexibility and control.
In the meantime I have written a workaround which should help you.

Look here:
http://code.google.com/p/snakeyaml/source/browse/src/test/java/org/yaml/snakeyaml/resolver/ImplicitResolverTest.java

Changes:
1) use latest source. BaseConstructor gets a minor change.
http://code.google.com/p/snakeyaml/source/detail?r=884b6d11baa6d25b18df4bb6c31af7df1b23660f
The details are here: http://code.google.com/p/snakeyaml/issues/detail?id=42

2) ConfigurationConstructor overwrites 'getConstructor()' to force the
custom constructor

3) minor:


yaml.addImplicitResolver("!cfg", Pattern.compile("\\$\\([a-zA-Z\\d\

\u002E\\u005F]+\\)"),"$");

'$' instead of 'null' at the end slightly improves performance since
the regular expression is only used when the value starts with '$'.

Hopefully it helps you.

Feel free to propose any idea/solution you find useful.


P.S. In the version 1.6 I plan to refactor the way tags are used in
SnakeYAML. Currently they are simply Strings. I am going to introduce
Tag class. So instead of '!cfg' you will use 'new Tag("!cfg")'.
Unfortunately this is a backward-incompatible change.

-
Andrey

Artem

unread,
Jan 9, 2010, 10:38:54 AM1/9/10
to SnakeYAML
Thanks for solution.
I have some comments/questions to current implementation:

1)
As i understand, before using Constructor tags(even implicit) for all
nodes already resolved.
I think that tag specify _how_ object for that node will be created,
not just a type or class or anything else.
Hence we need no more logic to construct Java Object tree from Node
tree with resolved tags than list of constructors for each tag.
For example if you have !!float tag on some scalar node kind of object
created in constructor depends on type(target class) specified in Node
object.
You should not ignore tags(which done in issue #40) but in constructor
for the 'float' tag use logic as in
Constructor.constructStandardJavaInstance method.
It is just a proposal and I understand what it is a big refactoring
but that architecture must simplify tag processing in SnakeYAML.

From the other side: if type(class) of the Node is unknown for tag
constructor? SnakeYAML provide ability to create object of such
classes if it have constructor with one argument, but i think it's
slightly dirty way.
It will be great if for each standard tag library will provide ability
to define custom Converter. For example with such interface:

public abstract class Converter<T>
{
public final Class<T> getRelatedClass(){return T.class}
public abstract T convert(ScalarNode node);
}

public class FileConverter<java.io.File>
{
public java.io.File convert(ScalarNode node)
{
return new File(node.getValue());
}
}

Even logic as in Constructor.constructStandardJavaInstance method may
be turned into search of necessary converter:

Map<Class<?>, Converter> floatConverters = converters.get("!!float");
if (type == Float.class || type == Double.class || type == Float.TYPE
|| type == Double.TYPE || type ==
BigDecimal.class) {
if (type == BigDecimal.class) {
result = new BigDecimal(node.getValue());
} else {
Construct doubleConstructor = yamlConstructors.get
(Tags.FLOAT);
result = doubleConstructor.construct(node);
if (type == Float.class || type == Float.TYPE) {
result = new Float((Double) result);
}
}

Converter conv = floatConverters.get(node.getType());
if(conv == null)
throw new YAMLException("Converter not found for type " +
node.getType() + " and tag "+node.getTag());
else
return conv.convert(node);

Class<?> can be replaced by Type to support primitives and Converter
itself may be a part of Constructor.

Huh. Summary, for my previous problem with implicit tags we will have
explicit:) solution - only Construct for my tag !cfg will be called
without additional processing inside the library.

P.S. Merge key(<<) does not work inside a bean declaration, i think it
is a issue, isn't it?

--
Artem

Artem

unread,
Jan 9, 2010, 10:47:48 AM1/9/10
to SnakeYAML
Following code from SnakeYAML source. I forgot to remove it:)

On Jan 9, 6:38 pm, Artem <jasp...@gmail.com> wrote:
---


> if (type == Float.class || type == Double.class || type == Float.TYPE
>                     || type == Double.TYPE || type ==
> BigDecimal.class) {
>                 if (type == BigDecimal.class) {
>                     result = new BigDecimal(node.getValue());
>                 } else {
>                     Construct doubleConstructor = yamlConstructors.get
> (Tags.FLOAT);
>                     result = doubleConstructor.construct(node);
>                     if (type == Float.class || type == Float.TYPE) {
>                         result = new Float((Double) result);
>                     }
>                 }
>

---

Artem

Andrey

unread,
Jan 9, 2010, 1:20:37 PM1/9/10
to SnakeYAML
>As i understand, before using Constructor tags(even implicit) for all
>nodes already resolved.
Yes, they are.

>I think that tag specify _how_ object for that node will be created,
>not just a type or class or anything else.

Yes. To be precise which implementation of the Construct interface to
use to create an instance

>Hence we need no more logic to construct Java Object tree from Node
>tree with resolved tags than list of constructors for each tag.

Not exactly.
a) Let us see an example.
---
value: 3
...

We want to parse a JavaBean:
class MyBean {
private String value;
<getter and setter>
}

'3' has the implicit tag !!int but MyBean expect a String instead. In
this case the tag will be ignored in favor of the runtime class.

b) we do not need a list of constructors for each tag. We need exactly
one constructor.

>For example if you have !!float tag on some scalar node kind of object
>created in constructor depends on type(target class) specified in Node object.

I think it is in the contradiction with the previous statement.
Also the class to be instantiated is not always known. The parsed
document does not have to be a JavaBean.

I am afraid I do not understand how to apply the code into the
SnakeYAML core. May I propose the following:
Since we use Mercurial it is very easy to create a clone (http://
code.google.com/p/snakeyaml/source/createClone) . You can try to show
the change in the repository. Because SnakeYAML has a very high test
coverage (98%) we can relatively easy change any code and even the
architecture. Feel free to contribute !

I am not very happy with the current implementation. I was trying to
stay as close as possible to PyYAML API but Python and Java are so
much different that this intention creates issues for complex cases.
The goal was that Python and Java developers use the same API and feel
home if they need to parse a document coming from another languages.
Apparently now it looks like these 2 languages are like 2 universes
and they never intersect.
I do not mind if we dramatically change the API and the architecture
if it provides real advantages over existing code base.

>P.S. Merge key(<<) does not work inside a bean declaration, i think it
>is a issue, isn't it?

It is. Please create an issue.

-
Andrey

Reply all
Reply to author
Forward
0 new messages