This article discusses extending final implementation classes through the use of Proxy
InvocationHandler
s and Default Interface Methods introduced in Java 8. The specific use case described here is to add fluent methods to Document Object Model (DOM) to enable Javadoc implementations to provide snippets of well-formed HTML/XML. The various fluent add()
methods implemented in FluentNode
(the “facade”):
default FluentNode add(Stream<Node> stream) {
return add(stream.toArray(Node[]::new));
}
default FluentNode add(Iterable<Node> iterable) {
return add(toArray(iterable));
}
default FluentNode add(Node... nodes) {
for (Node node : nodes) {
switch (node.getNodeType()) {
case ATTRIBUTE_NODE:
getAttributes().setNamedItem(node);
break;
default:
appendChild(node);
break;
}
}
return this;
}
Enter fullscreen mode Exit fullscreen mode
which allows the creation of fluent methods to create Element
s:
default FluentNode element(String name, Stream<Node> stream) {
return element(name, stream.toArray(Node[]::new));
}
default FluentNode element(String name, Iterable<Node> iterable) {
return element(name, toArray(iterable));
}
default FluentNode element(String name, Node... nodes) {
return ((FluentNode) owner().createElement(name)).add(nodes);
}
Enter fullscreen mode Exit fullscreen mode
that can be built up in to templates (e.g., HTMLTemplates
):
default FluentNode pre(String content) {
return element("pre").content(content);
}
Enter fullscreen mode Exit fullscreen mode
Allowing the creation of methods to be trivially invoked to return XML snippets:
return a(href, text)
.add(map.entrySet()
.stream()
.map(t -> attr(t.getKey(), t.getValue())));
Enter fullscreen mode Exit fullscreen mode
to produce something like
<a href="https://www.rfc-editor.org/rfc/rfc2045.txt" target="newtab">RFC2045</a>
Enter fullscreen mode Exit fullscreen mode
Complete javadoc is provided.
Theory of Operation
An application can add a facade to a class hierarchy by extending FacadeProxyInvocationHandler
1 and implementing getProxyClassFor(Object)
where the invoke(Object,Method,Object[])
“enhances” any eligible return types. Conceptually:
public Object enhance(Object in) {
Object out = null;
Class<?> type = getProxyClassFor(in);
if (type != null) {
try {
out =
type.getConstructor(InvocationHandler.class)
.newInstance(this);
} catch (RuntimeException exception) {
throw exception;
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
}
return (out != null) ? out : in;
}
protected abstract Class<?> getProxyClassFor(Object object);
@Override
public Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
Object result = super.invoke(proxy, method, argv);
return enhance(result);
}
Enter fullscreen mode Exit fullscreen mode
There are additional details that are discussed in the next section. The implementor must return a interface Class
to Proxy
from getProxyClassFor(Object)
for any Class
to be enhanced.
Implementation
Node
will be enhanced by FluentNode
and Document
will be enhanced by FluentDocument
. Note: A Node
does not necessarily have to implement the sub-interface that corresponds to Node.getNodeType() so both the Object
‘s class hierarchy and node type are analyzed and the results are cached in the getProxyClassFor(Object)
implementation.
private final HashMap<List<Class<?>>,Class<?>> map = new HashMap<>();
...
@Override
protected Class<?> getProxyClassFor(Object object) {
Class<?> type = null;
if (object instanceof Node && (! (object instanceof FluentNode))) {
Node node = (Node) object;
List<Class<?>> key =
Arrays.asList(NODE_TYPE_MAP.getOrDefault(node.getNodeType(), Node.class),
node.getClass());
type = map.computeIfAbsent(key, k -> compute(k));
}
return type;
}
private Class<?> compute(List<Class<?>> key) {
LinkedHashSet<Class<?>> implemented =
key.stream()
.flatMap(t -> getImplementedInterfacesOf(t).stream())
.filter(t -> Node.class.isAssignableFrom(t))
.filter(t -> Node.class.getPackage().equals(t.getPackage()))
.collect(Collectors.toCollection(LinkedHashSet::new));
LinkedHashSet<Class<?>> interfaces =
implemented.stream()
.map(t -> fluent(t))
.filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
interfaces.addAll(implemented);
new ArrayList<>(interfaces)
.stream()
.forEach(t -> interfaces.removeAll(Arrays.asList(t.getInterfaces())));
return getProxyClass(interfaces.toArray(new Class<?>[] { }));
}
Enter fullscreen mode Exit fullscreen mode
The corresponding “fluent” interface is found through reflection:
private Class<?> fluent(Class<?> type) {
Class<?> fluent = null;
if (Node.class.isAssignableFrom(type) && Node.class.getPackage().equals(type.getPackage())) {
try {
String name =
String.format("%s.Fluent%s",
FluentNode.class.getPackage().getName(),
type.getSimpleName());
fluent = Class.forName(name).asSubclass(FluentNode.class);
} catch (Exception exception) {
}
}
return fluent;
}
Enter fullscreen mode Exit fullscreen mode
The DocumentBuilderFactory
, FluentDocumentBuilderFactory
, and DocumentBuilder
, FluentDocument.Builder
, implementations are both straightforward. The two DocumentBuilder
methods that create new Document
s are implemented by creating a new FluentNode.InvocationHandler
:
@Override
public FluentDocument newDocument() {
Document document = builder.newDocument();
return (FluentDocument) new FluentNode.InvocationHandler().enhance(document);
}
@Override
public Document parse(InputSource in) throws SAXException, IOException {
Document document = builder.parse(in);
return (FluentDocument) new FluentNode.InvocationHandler().enhance(document);
}
Enter fullscreen mode Exit fullscreen mode
Creating a new FluentDocument
is as simple as:
document =
FluentDocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument();
Enter fullscreen mode Exit fullscreen mode
Unfortunately, the implementation as described so far will fail with an error similar to:
...
[ERROR] Caused by: org.w3c.dom.DOMException: WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it.
[ERROR] at com.sun.org.apache.xerces.internal.dom.AttributeMap.setNamedItem(AttributeMap.java:86)
[ERROR] at ball.xml.FluentNode.add(FluentNode.java:180)
...
[ERROR] ... 35 more
...
Enter fullscreen mode Exit fullscreen mode
The com.sun.org.apache.xerces.internal.dom
implementation classes expect to have package access to other package classes. This requires adjusting the invoke(Object,Method,Object[])
implementation to choose the wider of the Proxy
facade or the reverse depending on the required context:
@Override
public Object invoke(Object proxy, Method method, Object[] argv) throws Throwable {
Object result = null;
Class<?> declarer = method.getDeclaringClass();
Object that = map.reverse.get(proxy);
if (declarer.isAssignableFrom(Object.class)) {
result = method.invoke(that, argv);
} else {
argv = reverseFor(method.getParameterTypes(), argv);
if (declarer.isAssignableFrom(that.getClass())) {
result = method.invoke(that, argv);
} else {
result = super.invoke(proxy, method, argv);
}
}
return enhance(result);
}
Enter fullscreen mode Exit fullscreen mode
This requires keeping an IdentityHashMap
of enhanced Object
to Proxy
and reverse:
private final ProxyMap map = new ProxyMap();
public Object enhance(Object in) {
Object out = null;
if (! hasFacade(in)) {
Class<?> type = getProxyClassFor(in);
if (type != null) {
out = map.computeIfAbsent(in, k -> compute(type));
}
}
return (out != null) ? out : in;
}
private <T> T compute(Class<T> type) {
T proxy = null;
try {
proxy =
type.getConstructor(InvocationHandler.class)
.newInstance(this);
} catch (RuntimeException exception) {
throw exception;
} catch (Exception exception) {
throw new IllegalStateException(exception);
}
return proxy;
}
private class ProxyMap extends IdentityHashMap<Object,Object> {
private final IdentityHashMap<Object,Object> reverse = new IdentityHashMap<>();
public IdentityHashMap<Object,Object> reverse() { return reverse; }
@Override
public Object put(Object key, Object value) {
reverse().put(value, key);
return super.put(key, value);
}
}
Enter fullscreen mode Exit fullscreen mode
and providing the necessary “reverse” methods contained in the source.
Integration
AbstractTaglet
demonstrates the integration. The class must implement XMLServices
and provide an implementation of document()
.
private final FluentDocument document;
...
protected AbstractTaglet(boolean isInlineTag, boolean inPackage,
boolean inOverview, boolean inField,
boolean inConstructor, boolean inMethod,
boolean inType) {
...
try {
...
document =
FluentDocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.newDocument();
document
.add(element("html",
element("head",
element("meta",
attr("charset", "utf-8"))),
element("body")));
} catch (Exception exception) {
throw new ExceptionInInitializerError(exception);
}
}
...
@Override
public FluentDocument document() { return document; }
Enter fullscreen mode Exit fullscreen mode
AbstractTaglet
also implements HTMLTemplates
which provides default
methods for HTML elements/nodes. HTMLTemplates
is further extended by JavadocHTMLTemplates
to provide common HTML/XML fragments required to generate Javadoc.
Summary
The FacadeProxyInvocationHandler
combined with specialized interfaces implementing default
methods provides a mechanism for extending an otherwise final
class hierarchy.
[1] FacadeProxyInvocationHandler
is a subclass of DefaultInvocationHandler
whose invoke(Object,Method,Object[])
implementation is discussed in “Adding Support to Java InvocationHandler Implementations for Interface Default Methods”). ↩
暂无评论内容