Saturday, July 05, 2014

Modern XML to Java

How many frameworks are there for converting XML to Java? Hard to count. As an experiment I tried my hand at one. I have two top-level classes plus an annotation:

public final class XMLFuzzy
        implements InvocationHandler {
    private static final XPath xpath = XPathFactory.newInstance().
            newXPath();
    private static final Map<Method, XPathExpression> expressions
     = new ConcurrentHashMap<>();

    private final Node node;
    private final Converter converter;

    public static final class Factory {
        private final Converter converter;

        public Factory(final Converter converter) {
            this.converter = converter;
        }

        public <T> T of(@Nonnull final Class<T> itf,
                @Nonnull final Node node) {
            return XMLFuzzy.of(itf, node, converter);
        }
    }

    public static <T> T of(@Nonnull final Class<T> itf,
     @Nonnull final Node node,
            @Nonnull final Converter converter) {
        return itf.cast(newProxyInstance(itf.getClassLoader(),
                new Class[]{itf},
                new XMLFuzzy(node, converter)));
    }

    private XMLFuzzy(final Node node, final Converter converter) {
        this.node = node;
        this.converter = converter;
    }

    @Override
    public Object invoke(final Object proxy, final Method method,
         final Object[] args)
            throws Throwable {
        return converter.convert(method.getReturnType(), expressions.
                computeIfAbsent(method, XMLFuzzy::compile).
                evaluate(node));
    }

    private static XPathExpression compile(@Nonnull final Method method) {
        final String expression = asList(method.getAnnotations()).stream().
                filter(From.class::isInstance).
                map(From.class::cast).
                findFirst().
                orElseThrow(() -> new MissingAnnotation(method)).
                value();
        try {
            return xpath.compile(expression);
        } catch (final XPathExpressionException e) {
            throw new BadXPath(method, expression, e);
        }
    }

    public static final class MissingAnnotation
            extends RuntimeException {
        private MissingAnnotation(final Method method) {
            super(format("Missing @X(xpath) annotation: %s", method));
        }
    }

    public static final class BadXPath
            extends RuntimeException {
        private BadXPath(final Method method, final String expression,
                final XPathExpressionException e) {
            super(format("Bad @X(xpath) annotation on '%s': %s: %s",
                    method, expression, e.getMessage()));
            setStackTrace(e.getStackTrace());
        }
    }
}

I have left out Converter; it turns strings into objects of a given type, another example of overimplemented framework code in Java. And the annotation:

@Documented
@Inherited
@Retention(RUNTIME)
@Target(METHOD)
public @interface From {
    String value();
}

The idea is straight-forward: drive the object mapping from XML with XPaths. Credit to XMLBeam for introducing to me the elegant use of JDK proxies for this purpose.

Of course tests:

public final class XMLFuzzyTest {
    private Top top;

    @Before
    public void setUp()
            throws ParserConfigurationException, IOException, SAXException {
        final Document document = DocumentBuilderFactory.newInstance().
                newDocumentBuilder().
                parse(new InputSource(new StringReader(XML)));
        top = new XMLFuzzy.Factory(new Converter()).of(Top.class, document);
    }

    @Test
    public void shouldHandleString() {
        assertThat(top.a(), is(equalTo("apple")));
    }

    @Test
    public void shouldHandlePrimitiveInt() {
        assertThat(top.b(), is(equalTo(3)));
    }

    @Test
    public void shouldHandleRURI() {
        assertThat(top.c(), is(equalTo(URI.create("http://some/where"))));
    }

    @Test(expected = MissingAnnotation.class)
    public void shouldThrowOnMissingAnnotation() {
        top.d();
    }

    @Test(expected = BadXPath.class)
    public void shouldThrowOnBadXPath() {
        top.e();
    }

    public interface Top {
        // For the purposes of this blog post, pretend Java supports
 // multiline string literals
        @Language("XML")
        String XML = "<top>
                    <a>apple</a>
      <b>3</b>
      <c>http://some/where</c>
         </top>";

        @From("//top/a")
        String a();

        @From("//top/b")
        int b();

        @From("//top/c")
        URI c();

        void d();

        @From("dis' ain't xpath")
        void e();
    }
}

No comments: