Entries tagged ‘rhino-collider’
XStream is a cool library for serializing Java objects to XML files, and back again. Given that I prefer to work with JavaScript, rather than Java, I spent some time today trying to make XStream work with Rhino objects.
First, a quick example of XStream, from their two minute tutorial. We start with two small Java classes:
public class Person {
private String firstname;
private String lastname;
private PhoneNumber phone;
private PhoneNumber fax;
// ... constructors and methods
}
public class PhoneNumber {
private int code;
private String number;
// ... constructors and methods
}
Set up XStream:
XStream xstream = new XStream();
xstream.alias("person", Person.class);
xstream.alias("phonenumber", PhoneNumber.class);
And instantiate the classes, plus serialize to XML:
Person joe = new Person("Joe", "Walnes");
joe.setPhone(new PhoneNumber(123, "1234-456"));
joe.setFax(new PhoneNumber(123, "9999-999"));
String xml = xstream.toXML(joe);
And here’s the result:
<person>
<firstname>Joe</firstname>
<lastname>Walnes</lastname>
<phone>
<code>123</code>
<number>1234-456</number>
</phone>
<fax>
<code>123</code>
<number>9999-999</number>
</fax>
</person>
Good, readable XML, as a serialization from Java objects. Going back is as easy as:
Person newJoe = (Person)xstream.fromXML(xml);
XStreaming Rhino
Unfortunately, things aren’t as easy when you try to serialize a JavaScript object from within Rhino:
xstream = new Packages.com.thoughtworks.xstream.XStream();
xstream.toXML({foo: 'bar'});
This results in a rather long error stack trace, which boils down to:
org.mozilla.javascript.WrappedException: Wrapped com.thoughtworks.xstream.converters.ConversionException: Could not call org.mozilla.javascript.NativeObject.writeObject()
---- Debugging information ----
message : Could not call org.mozilla.javascript.NativeObject.writeObject()
cause-message : null
cause-exception : java.lang.reflect.InvocationTargetException
-------------------------------
The problem here is that Rhino objects can’t be directly serialized by Java classes. Luckily, XStream is quite flexible and allows you to write your own serializer. Or, in XStream terms, Converter. So, that’s what I did:
xstream = new Packages.com.thoughtworks.xstream.XStream();
converter = RhinoConverter.create();
xstream.registerConverter(converter);
xstream.toXML({foo: 'bar'});
This gives:
<org.mozilla.javascript.NativeObject>
<object>
<foo type="string">bar</foo>
</object>
</org.mozilla.javascript.NativeObject>
Before we continue, I’ve put the code up on Github. Here’s an example file.
If we serialize the following object:
{
'foo': 'bar',
'answer': 42,
'truth': true,
'falsehood': false,
'nothingness': null,
'everything': undefined,
'nested': {
'birds': 'nest'
},
'feist': [1, 2, 3, 4],
'now': new Date
}
We get:
<org.mozilla.javascript.NativeObject>
<object>
<foo type="string">bar</foo>
<answer type="number">42.0</answer>
<truth type="boolean">true</truth>
<falsehood type="boolean">false</falsehood>
<nothingness type="null"/>
<everything type="undefined"/>
<nested>
<object>
<birds type="string">nest</birds>
</object>
</nested>
<feist>
<array>
<length type="number">4</length>
<_0 type="number">1.0</_0>
<_1 type="number">2.0</_1>
<_2 type="number">3.0</_2>
<_3 type="number">4.0</_3>
</array>
</feist>
<now>
<date stamp="2008-11-09T23:13:26.282+01:00"/>
</now>
</object>
</org.mozilla.javascript.NativeObject>
What’s interesting here is that only the XML root element is org.mozilla.javascript.NativeObject. This root element is created by XStream, and is a serialization of the main object. From XStream’s point of view, this main object is a Java object, as modelled by Rhino. The converter itself, however, is written in JavaScript. All descendent elements are serializations of JavaScript objects. That’s why <nested> doesn’t contain a <org.mozilla.javascript.NativeObject>.
The constructors of the JavaScript objects are retained in the XML, in the form of <object>, <array> and <date> elements. Children of these elements are the object’s properties. Primitive data types are indicated by the property having a type attribute, and a raw value. <date> elements have an extra stamp attribute, which is the value of the original Date object in RFC 3339 format. Because XML property names may not start with a digit, the array indices are prefixed by an underscore. (And yes, underscores themselves are escaped by another underscore. _ is serialized to __.)
It’s possible to add custom constructors to the converter:
function Klass() {
this.property = 'foobar';
}
Klass.prototype.print = function() {
print(this.property);
};
converter.register('klass', Klass);
k = new Klass;
k.property = 'hello world';
xml = xstream.toXML(k);
Here I’ve created a class Klass, and registered it with the converter we created earlier. This does run against XStream’s alias() method, which you saw earlier in the XStream example. This is because XStream deals with Java objects, and it’s our JavaScript converter that deals with JavaScript objects.
The XML serialization is as follows:
<org.mozilla.javascript.NativeObject>
<klass>
<property type="string">hello world</property>
</klass>
</org.mozilla.javascript.NativeObject>
We can now deserialize the XML, and run Klass.prototype.print(), to see if deserialization worked:
k1 = xstream.fromXML(xml);
k1.print(); // 'hello world'
Marvelous!
Look ma, no constructor!
Here’s the code which creates an instance for deserialized objects:
object = {};
object.__proto__ = constructor.prototype;
This is where the JavaScript truly shines. Rather than doing object = new constructor(), where constructor is a previously derived reference to a constructor method, we simply create an object and point it to the prototype of the constructor, without invoking the constructor method!
At this point it may help to explain how JavaScript finds a property on an object. If we have object o, and we try to resolve o.foo, JavaScript first checks if o has a property foo. If it does, it just returns the value of this property. If it does not, JavaScript checks if the object referenced by o.__proto__ has the property. If it does, the value is returned, if it doesn’t, o.__proto__.__proto__ is checked. This continues until either foo has been found, or we run out of __proto__ references. In JavaScript, __proto__ tends to point to a prototype object of a constructor method. Object.prototype for example. The __proto__ references form a prototype chain. Object.prototype is usually the last object in this chain.
As it turns out, we could program, say, new MyClass(), ourselves, like this:
function MyClass() {
this.foo = 'bar';
};
MyClass.prototype.baz = function() {
return 'thud';
};
var auto = new MyClass();
auto.foo; // 'bar'
auto.baz(); // 'thud'
var manual = {};
manual.__proto__ = MyClass.prototype;
MyClass.apply(manual, []);
manual.foo; // 'bar'
manual.baz(); // 'thud'
Strings
When working with Rhino, the Java versus JavaScript object issue comes up more often. In the converter code, you’ll often see something like '' + reader.getAttribute('type'). For example:
switch ('' + reader.getAttribute('type')) {
case 'string':
value = '' + reader.getValue();
break;
The problem is that reader is a Java object, and reader.getAttribute('type') returns a Java string. For some reason, Java strings don’t match JavaScript strings in switch() statements, although they often do otherwise:
javaString = new Packages.java.lang.String('string');
javaString == 'string'; // true
switch(javaString) {
case 'string':
print('Match');
break;
default:
print('No match');
}
// 'No match'
Something to watch out for!
XStream & Rhino
Back to XStream. What I’ve built now is a basic implementation, which does not support duplicate or circular references. Neither does it serialize JavaScript functions, and I haven’t really tried serializing Java objects referenced by a JavaScript object. But, it’s a start! Let me know if you use it, find faults, or want to commit a fix.
