The Source for Java Technology Collaboration
User: Password:



Sergey Malenkov

Sergey Malenkov's Blog

How to encode Type-Safe Enums?

Posted by malenkov on August 09, 2006 at 10:00 PM | Comments (8)

This is a sequel of my first article about Enums encoding.
As you may know, Type-Safe Enums were proposed by Joshua Bloch in Effective Java,
but they are not supported by XMLEncoder. This article describes how to encode them into XML properly.

Let's create a simple Type-Safe Enum with static method to test encoding into XML:

import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;

public final class TestEnum {
    public static final TestEnum LEFT = new TestEnum( "move to left" );
    public static final TestEnum RIGHT = new TestEnum( "move to right" );

    private final String info;

    private TestEnum( String name ) {
        this.info = name;
    }

    public String getInfo() {
        return this.info;
    }

    public static void main( String[] args ) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();

        XMLEncoder encoder = new XMLEncoder( stream );
        encoder.writeObject( TestEnum.LEFT );
        encoder.writeObject( TestEnum.RIGHT );
        encoder.close();

        System.out.println( stream );
    }
}

If the test method is executed it will print the following result:

java.lang.InstantiationException: TestEnum
Continuing ...
java.lang.Exception: XMLEncoder: discarding statement XMLEncoder.writeObject(TestEnum);
Continuing ...
java.lang.InstantiationException: TestEnum
Continuing ...
java.lang.Exception: XMLEncoder: discarding statement XMLEncoder.writeObject(TestEnum);
Continuing ...
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.5.0_07" class="java.beans.XMLDecoder">
</java>

As you can see, some exceptions were thrown during encoding and XML does not contain any object.

Let's change the test program. We should create persistence delegate and register it before encoding:

import java.beans.Encoder;
import java.beans.Expression;
import java.beans.PersistenceDelegate;
import java.beans.XMLEncoder;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public final class TestEnum {
    public static final TestEnum LEFT = new TestEnum( "move to left" );
    public static final TestEnum RIGHT = new TestEnum( "move to right" );

    private final String info;

    private TestEnum( String name ) {
        this.info = name;
    }

    public String getInfo() {
        return this.info;
    }

    public static void main( String[] args ) {
        ByteArrayOutputStream stream = new ByteArrayOutputStream();

        XMLEncoder encoder = new XMLEncoder( stream );
        encoder.setPersistenceDelegate( TestEnum.class, new TypeSafeEnumPersistenceDelegate() );
        encoder.writeObject( TestEnum.LEFT );
        encoder.writeObject( TestEnum.RIGHT );
        encoder.close();

        System.out.println( stream );
    }
}

class TypeSafeEnumPersistenceDelegate extends PersistenceDelegate {
    protected boolean mutatesTo( Object oldInstance, Object newInstance ) {
        return oldInstance == newInstance;
    }

    protected Expression instantiate( Object oldInstance, Encoder out ) {
        Class type = oldInstance.getClass();
        if ( !Modifier.isPublic( type.getModifiers() ) )
            throw new IllegalArgumentException( "Could not instantiate instance of non-public class: " + oldInstance );

        for ( Field field : type.getFields() ) {
            int mod = field.getModifiers();
            if ( Modifier.isPublic( mod ) && Modifier.isStatic( mod ) && Modifier.isFinal( mod ) && ( type == field.getDeclaringClass() ) ) {
                try {
                    if ( oldInstance == field.get( null ) )
                        return new Expression( oldInstance, field, "get", new Object[]{null} );
                } catch ( IllegalAccessException exception ) {
                    throw new IllegalArgumentException( "Could not get value of the field: " + field, exception );
                }
            }
        }
        throw new IllegalArgumentException( "Could not instantiate value: " + oldInstance );
    }
}

Note that TypeSafeEnumPersistenceDelegate does not depend on concrete class.
So, it is possible to use one instance of such persistence delegate with any class,
that contains public static final fields with the same type,
but it is necessary to register it with this concrete class.

As a result, XML contains both encoded objects:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.5.0_07" class="java.beans.XMLDecoder">
 <object class="TestEnum" field="LEFT"/>
 <object class="TestEnum" field="RIGHT"/>
</java>

I have bad news for you: JDK 6 does not support Type-Safe Enum encoding.
It is not so easy to integrate this code into DefaultPersistenceDelegate,
but I think I can do it for JDK 7 (it can affect other classes with public static final fields).
So, you should create custom persistence delegate and register it for your Type-Safe Enum.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • I have a great respect for the authors of XMLEncoder/XMLDecoder and the API. With that said, it has yet to gain any real traction. GUI builders like Matisse should be able to use it for persistence--that was a design goal. Yet, the only GUI builders that do so are for Buoy and not Swing.

    The package was obviously designed to be extended, yet written so that extension is difficult to impossible. Try writing debugging versions of XMLEncoder/XMLDecoder that give detailed diagnostics and error messages. You'll see what I mean. It's not too bad, if you can work within the package, but you are forced to essentially rewrite everything from scratch if you're implementing outside the package.

    -Curt

    Posted by: coxcu on August 10, 2006 at 08:19 AM

  • That is pretty cool. It immediately inspired me to try and write an encoder for an IDLEntity (CORBA). So far I’ve been unable to. Our IDLEntities are super simple, they just are a set of public fields. How do I tell the encoder to encode these fields?

    Posted by: aberrant on August 10, 2006 at 03:01 PM

  • Hi aberrant,
    I recommend you to add getter and setter for every field. JavaBeans properties are supported by DefaultPersistenceDelegate. If you don't want to add them, I'll post new blog with custom persistence delegate later.
    Sergey

    Posted by: malenkov on August 11, 2006 at 12:27 AM

  • Hi Curt,
    We plan to develop the declarative Swing XML for JDK 7.
    I am working on RFE 4864117 now. You can vote for it. XMLDecoder is fully redesigned, but I can integrate it only into JDK 7. XMLEncoder is more complex to redesign, because we should support backward compatibility.
    Sergey

    Posted by: malenkov on August 11, 2006 at 12:45 AM

  • Sergey,

    I'm glad this will be getting more attention. Here are some major limitations of XMLEncoder/XMLDecoder that you should consider fixing. They keep the API from seeing much wider use.

    RFE: Selective listener persistence
    http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4808251

    RFE: Define annotation to link constructor parameters to getters
    http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6176120

    Persistence mechanism not designed for development of internationalized apps
    http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4446154

    It's great to see that 6176120 is already fixed in Mustang. If a Builder interface makes it into Dolphin (which would be great) support for that should be added as well.

    - Curt

    Posted by: coxcu on August 11, 2006 at 09:06 AM

  • Hi Curt,
    RFE: Define annotation to link constructor parameters to getters
    This is partially fixed. All functionality are ready, but I should document it and use it instead registerConstructor.
    RFE: Selective listener persistence
    This problem is deep inside the XMLEncoder design. I'll think about it.
    Persistence mechanism not designed for development of internationalized apps
    I don't think that it is possible, because the string and (often) the container class does not have any information about resource. I recommend to create custom persistence delegate in such case.
    Sergey

    Posted by: malenkov on August 11, 2006 at 09:57 AM

  • Sergey,

    Selective listener persistence:

    The API was originally designed so that transience can vary
    by field, essentially mirroring the transient keyword. Unfortunately, listener lists of mixed transience are pretty common. Possible solutions include adding an optional TransienceDecider to Encoder constructors, supporting a Transient marker interface, or supporting a Transient class annotation. The TransientDecider is the most flexible of these, but usually class determines transience.


    Persistence mechanism not designed for development of internationalized apps

    I'm not sure how custom persistence delegates would solve this. If you think this is a good solution then please consider adding some examples to the documentation. In most cases, merely localizing the XML before decoding would seem to work.

    - Curt

    Posted by: coxcu on August 11, 2006 at 01:58 PM

  • Hello.

    Have a non-profit resourse on building large scale java applications:

    Type Safe Objects

    Thanks

    Posted by: screenedtwenty on June 04, 2007 at 05:06 PM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds