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();
}
}