Apparently I had more time on my hands than I thought. Warning: long,
detailed post ahead. I'll summarize in 2 points:
1. I have a small change to SnakeYaml that lets MappingNodes
representing Java classes be merged, correctly.
2. This change can be implemented as an optional extension to
SnakeYaml; SnakeYaml remains standards-compliant, with an option to
break spec for sake of usability.
Still reading? Alright then:
I now have a slightly hackish solution that allows for the feature I
requested, as a mostly-extension to SnakeYaml's existing
functionality. In fact, if I were to change existing SnakeYaml code
(rather than extending it), the changes would be very minor indeed.
With my changes, the following file (which does NOT conform to YAML
specifications):
---
- !!Data
&id001
a: 1
b: 1
- !!DataChild
<<: *id001
b: 3
c: 5
- !!DataUnrelated
<<: *id001
d: 7
...
Represents three objects:
1) A Data object, with values a=1, b=1.
2) A DataChild object, which extends Data to add a third field, c.
Values a=1 (merged), b=3 (overridden), c=5 (new).
3) A DataUnrelated object, which does NOT extend Data. Values are a=1
(merged), b=1 (merged), d=7 (new).
The output from parsing this and then printing the items in the
resulting list one-by-one is:
[output-start]
WARN: The specified merge source class Data is not an ancestor of the
merge-result class DataUnrelated
Data (a=1,b=1)
DataChild (a=1,b=3,c=5)
DataUnrelated (a=1,b=1,d=7)
[output-end]
The warning is emitted in lieu of a warning mechanism. This could,
depending on settings, be either ignored or the cause of an error.
The biggest requirement is that immediately before an object is
constructed, the mapping must be flattened, by
flattenMapping(MappingNode) in SafeConstructor.java . With just that
simple change, this issue is about 50% done.
My current implementation "hijacks" the Constructor class by
overriding the getClassForNode(Node) method:
@Override
protected Class<?> getClassForNode ( Node node ) {
flattenMapping ( (MappingNode)node );
return super.getClassForNode ( node );
}
This ends up being quite ugly in code because the
flattenMapping(MappingNode) method is private. The result is a
butchered copy-paste version of the method, obviously not an ideal
solution.
One important modification made to the flattenMapping(MappingNode)
method is to insert the code that issues the warning described above:
case mapping:
MappingNode mn = (MappingNode) valueNode;
// INSERTED WARNING-GEN CODE...
Class nodeClass = getClassForNode(node);
Class mergeSourceClass = getClassForNode(mn);
if ( !mergeSourceClass.isAssignableFrom ( nodeClass ) ) {
System.out.println ( "WARN: The specified merge source
"+mergeSourceClass.getCanonicalName()+" is not an ancestor of the
merge-result class "+nodeClass.getCanonicalName() );
}
// END INSERTED
flattenMapping(mn);
merge.addAll(mn.getValue());
break;
...
This is, at its heart, a literal one-line change. This change makes
use of all of SnakeYaml's existing functionality, implementing almost
nothing new. The only change is that instead of banning merging of
class objects, it assumes merging of class objects. I don't see any
security issues coming out of this, though maybe I'm just uncreative.
This also works just great with multiple-merges, as per (http://
yaml.org/type/merge.html).
Now, there are a lot of ways we could proceed with this knowledge.
First, this could be completely ignored. After all, the SnakeYaml team
undoubtedly places a lot of importance on being standards-compliant.
This (optional) extension breaks the YAML specification by allowing
merged nodes which do not have the !!merge tag. If this is your
decision, I will look to fork SnakeYaml for my own use.
Second, this could be facilitated, if not endorsed, by the SnakeYaml
team. A small code update would make it simpler to add this
functionality by exposing flattenMapping() as described above, either
with or without the warning code mentioned (or, with some kind of
boolean switch involved). This option seems the least helpful to
everyone involved.
Third, this could be added as an optional extension to SnakeYaml.
Explicitly marked as "BREAKS SPECIFICATION, DANGER DANGER", this seems
like a functionality that could prove helpful to the Java-only
community. This functionality would probably be implemented by an
extension of Constructor, with the caveat that flattenMapping() would
still need to be protected, not private. Alternately, this could be
given as an option to Constructor itself, avoiding any public-API
changes.
Thanks,
Jordan Angold