Skip to main content

Betwixt the Brackets

Posted by evanx on July 16, 2010 at 2:19 AM PDT

So we need to convert objects into XML and back again, eg. to store some data in the database in XML format, because otherwise maybe we just gonna have too many tables and joins and what-not.

Actually this is for a PNR, which is "Person Name Record" or something of that nature, as used in the travel industry for any booking eg. flights but also car hire, hotels, cruises etc. The other columns in the PNR table will be search terms for an agent to find a PNR, but otherwise all the gazillion details that passengers seem to require are stuffed into the XML, from meal preferences, seat preferences, who's got whose baby on their lap, and when was this baby born, not to mention various comments and remarks communicated betwixt the airline and the agents via this PNR.

Having used XStream before - which i must say is really great - i thought, let's try getting in and amongst Betwixt, not least because most of our third-party jars are from the Apache behemoth.

For a start, lets say we have an entity as follows.

public class Traveler extends IdEntity {    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    String firstNames;

    String surname;

    String email;

    String telephone;
   
    @Enumerated(EnumType.STRING)
    Salutation salutation;

    @Temporal(TemporalType.DATE)
    Date dateOfBirth;
    ...
}

So we write and read as follows.

    @Test
    public void testWrite() throws Exception {
        StringWriter outputWriter = new StringWriter();
        BeanWriter beanWriter = new BeanWriter(outputWriter);
        beanWriter.getXMLIntrospector().getConfiguration().setAttributesForPrimitives(false);
        beanWriter.getXMLIntrospector().getConfiguration().setTypeBindingStrategy(
            new CustomTypeBindingStrategy());
        beanWriter.getBindingConfiguration().setMapIDs(false);
        beanWriter.getBindingConfiguration().setObjectStringConverter(
            new CustomObjectStringConverter());
        beanWriter.enablePrettyPrint();
        Traveler traveler = new TravelerBean();
        traveler.setDateOfBirth(new GregorianCalendar(1969, 12, 31).getTime());
        traveler.setEmail("test@test.com");
        traveler.setSalutation(Salutation.MR);
        beanWriter.write("traveler", traveler);
        String xml = outputWriter.toString();
        outputWriter.close();
        System.out.println(xml);
        read(xml);
    }

    protected void read(String xml) throws Exception {
        StringReader xmlReader = new StringReader(xml);
        BeanReader beanReader = new BeanReader();
        beanReader.getXMLIntrospector().getConfiguration().setAttributesForPrimitives(false);
        beanReader.getXMLIntrospector().getConfiguration().setTypeBindingStrategy(
            new CustomTypeBindingStrategy());
        beanReader.getBindingConfiguration().setMapIDs(false);
        beanReader.getBindingConfiguration().setObjectStringConverter(
            new CustomObjectStringConverter());
        beanReader.registerBeanClass("traveler", Traveler.class);
        System.out.println(beanReader.parse(xmlReader));
    }

Googling the interwebs, we got the hint to use the following custom convertors and strategies eg. to handle enum's and what-not.

public class CustomObjectStringConverter extends ObjectStringConverter {

    final DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");

    @Override
    public String objectToString(Object object, Class type, Context context) {
        if (object == null) {
            return null;
        }
        if (type == Date.class) {
            return dateFormat.format((Date) object);
        }
        if (type.getSuperclass() == Enum.class) {
            return object.toString();
        }
        return super.objectToString(object, type, context);
    }

    @Override
    public Object stringToObject(String string, Class type, Context context) {
        if (string == null || string.length() == 0) {
            return null;
        }
        if (type == Date.class) {
            return formatDate(string);
        }
        if (type.getSuperclass() == Enum.class) {
            return Enum.valueOf(type, string);
        }
        return super.stringToObject(string, type, context);
    }

    protected Date formatDate(String string) {
        try {
            return dateFormat.parse(string);
        } catch (ParseException e) {
            throw new IllegalArgumentException(string, e);
        }
    }
}

What we probably should do above is check for annotations like @Temporal to see if our Date property is a date only, or a timestamp.

By the way, we can set an extended PropertySuppressionStrategy to suppress writing properties, and for reading XML, ElementSuppressionStrategy etc.

Finally, in order to make enum's work, we specify a BindingType.PRIMITIVE as follows.

public class CustomTypeBindingStrategy extends TypeBindingStrategy {

    @Override
    public BindingType bindingType(Class type) {
        if (isPrimitive(type)) {
            return BindingType.PRIMITIVE;
        }
        return BindingType.COMPLEX;
    }

    protected boolean isPrimitive(Class type) {
        if (type.isPrimitive()) {
            return true;
        } else if (type.equals(Object.class)) {
            return false;
        } else if (type.getName().startsWith("java.lang.")) {
            return true;
        } else if (type == Date.class) {
            return true;
        } else if (Enum.class.isAssignableFrom(type)) {
            return true;
        }
        return false;
    }
}

What else? Ja, you need quite a few commons jars like beanutils and digester 'cos they all build on each other apparently.

Related Topics >>