diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 26c70b80f7b..286fb346b26 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -212,8 +212,8 @@ - cglib - cglib + net.bytebuddy + byte-buddy diff --git a/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java b/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java index 347c219af7b..008e9beab5e 100644 --- a/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java +++ b/exist-core/src/main/java/org/exist/dom/persistent/NodeProxy.java @@ -681,6 +681,8 @@ public static int nodeType2XQuery(final short nodeType) { return Type.ATTRIBUTE; case Node.TEXT_NODE: return Type.TEXT; + case Node.CDATA_SECTION_NODE: + return Type.CDATA_SECTION; case Node.PROCESSING_INSTRUCTION_NODE: return Type.PROCESSING_INSTRUCTION; case Node.COMMENT_NODE: diff --git a/exist-core/src/main/java/org/exist/xmldb/LocalXMLResource.java b/exist-core/src/main/java/org/exist/xmldb/LocalXMLResource.java index 17af5fbc530..5e473fae9c3 100644 --- a/exist-core/src/main/java/org/exist/xmldb/LocalXMLResource.java +++ b/exist-core/src/main/java/org/exist/xmldb/LocalXMLResource.java @@ -28,6 +28,8 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.file.Files; import java.nio.file.Path; @@ -38,6 +40,10 @@ import javax.annotation.Nullable; import javax.xml.transform.TransformerException; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.InvocationHandlerAdapter; +import net.bytebuddy.matcher.ElementMatchers; import org.apache.commons.io.IOUtils; import org.exist.dom.memtree.DocumentImpl; import org.exist.dom.memtree.NodeImpl; @@ -72,10 +78,6 @@ import com.evolvedbinary.j8fu.function.ConsumerE; import com.evolvedbinary.j8fu.tuple.Tuple3; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - /** * Local implementation of XMLResource. */ @@ -329,20 +331,32 @@ private Node exportInternalNode(final Node node) { throw new IllegalArgumentException("Provided node does not implement org.w3c.dom"); } - final Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(domClazz.get()); - final Class[] interfaceClasses; + DynamicType.Builder byteBuddyBuilder = new ByteBuddy() + .subclass(domClazz.get()); + + // these interfaces are just used to flag the node type (persistent or memtree) to make + // the implementation of {@link DOMMethodInterceptor} simpler. if (node instanceof StoredNode) { - interfaceClasses = new Class[]{domClazz.get(), StoredNodeIdentity.class}; + byteBuddyBuilder = byteBuddyBuilder.implement(StoredNodeIdentity.class); } else if (node instanceof org.exist.dom.memtree.NodeImpl) { - interfaceClasses = new Class[]{domClazz.get(), MemtreeNodeIdentity.class}; - } else { - interfaceClasses = new Class[] { domClazz.get() }; + byteBuddyBuilder = byteBuddyBuilder.implement(MemtreeNodeIdentity.class); } - enhancer.setInterfaces(interfaceClasses); - enhancer.setCallback(new DOMMethodInterceptor(node)); - return (Node)enhancer.create(); + byteBuddyBuilder = byteBuddyBuilder + .method(ElementMatchers.any()) + .intercept(InvocationHandlerAdapter.of(new DOMMethodInterceptor(node))); + + try { + final Node nodeProxy = byteBuddyBuilder + .make() + .load(getClass().getClassLoader()) + .getLoaded() + .getDeclaredConstructor().newInstance(); + + return nodeProxy; + } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException(e.getMessage(), e); + } } private Optional> getW3cNodeInterface(final Class nodeClazz) { @@ -352,7 +366,7 @@ private Optional> getW3cNodeInterface(final Class (Class)c); } - private class DOMMethodInterceptor implements MethodInterceptor { + public class DOMMethodInterceptor implements InvocationHandler { private final Node node; public DOMMethodInterceptor(final Node node) { @@ -360,7 +374,9 @@ public DOMMethodInterceptor(final Node node) { } @Override - public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable { + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + + /* NOTE(AR): we have to take special care of eXist-db's persistent and memtree DOM's node equality. @@ -381,9 +397,9 @@ public Object intercept(final Object obj, final Method method, final Object[] ar */ Object domResult = null; if(method.getName().equals("equals") - && obj instanceof StoredNodeIdentity + && proxy instanceof StoredNodeIdentity && args.length == 1 && args[0] instanceof StoredNodeIdentity) { - final StoredNodeIdentity ni1 = ((StoredNodeIdentity) obj); + final StoredNodeIdentity ni1 = ((StoredNodeIdentity) proxy); final StoredNodeIdentity ni2 = ((StoredNodeIdentity) args[0]); final Optional niEquals = ni1.getNodeId().flatMap(n1id -> ni2.getNodeId().map(n1id::equals)); @@ -391,9 +407,9 @@ public Object intercept(final Object obj, final Method method, final Object[] ar domResult = niEquals.get(); } } else if(method.getName().equals("equals") - && obj instanceof MemtreeNodeIdentity + && proxy instanceof MemtreeNodeIdentity && args.length == 1 && args[0] instanceof MemtreeNodeIdentity) { - final MemtreeNodeIdentity ni1 = ((MemtreeNodeIdentity) obj); + final MemtreeNodeIdentity ni1 = ((MemtreeNodeIdentity) proxy); final MemtreeNodeIdentity ni2 = ((MemtreeNodeIdentity) args[0]); final Optional niEquals = ni1.getNodeId().flatMap(n1id -> ni2.getNodeId().map(n2id -> n1id._1 == n2id._1 && n1id._2 == n2id._2 && n1id._3 == n2id._3)); @@ -401,12 +417,12 @@ public Object intercept(final Object obj, final Method method, final Object[] ar domResult = niEquals.get(); } } else if(method.getName().equals("getNodeId")) { - if (obj instanceof StoredNodeIdentity - && args.length == 0 + if (proxy instanceof StoredNodeIdentity + && (args == null || args.length == 0) && node instanceof StoredNode) { domResult = Optional.of(((StoredNode) node).getNodeId()); - } else if (obj instanceof MemtreeNodeIdentity - && args.length == 0 + } else if (proxy instanceof MemtreeNodeIdentity + && (args == null || args.length == 0) && node instanceof org.exist.dom.memtree.NodeImpl) { final org.exist.dom.memtree.NodeImpl memtreeNode = (org.exist.dom.memtree.NodeImpl) node; domResult = Optional.of(Tuple(memtreeNode.getOwnerDocument(), memtreeNode.getNodeNumber(), memtreeNode.getNodeType())); @@ -447,7 +463,7 @@ public int getLength() { * Used by {@link DOMMethodInterceptor} to * help with equality of persistent DOM nodes. */ - private interface StoredNodeIdentity { + public interface StoredNodeIdentity { Optional getNodeId(); } @@ -455,7 +471,7 @@ private interface StoredNodeIdentity { * Used by {@link DOMMethodInterceptor} to * help with equality of memtree DOM nodes. */ - private interface MemtreeNodeIdentity { + public interface MemtreeNodeIdentity { Optional> getNodeId(); } diff --git a/exist-parent/pom.xml b/exist-parent/pom.xml index 63b77c4ff30..ec55e09682f 100644 --- a/exist-parent/pom.xml +++ b/exist-parent/pom.xml @@ -439,9 +439,9 @@ - cglib - cglib - 3.3.0 + net.bytebuddy + byte-buddy + 1.14.4 @@ -755,7 +755,7 @@ org.jacoco jacoco-maven-plugin - 0.8.8 + 0.8.9 jacocoArgLine diff --git a/extensions/exquery/restxq/pom.xml b/extensions/exquery/restxq/pom.xml index 78f0db66c72..e1ab8b81232 100644 --- a/extensions/exquery/restxq/pom.xml +++ b/extensions/exquery/restxq/pom.xml @@ -71,8 +71,8 @@ - cglib - cglib + net.bytebuddy + byte-buddy diff --git a/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapter.java b/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapter.java index 4e5a6473a3f..0beda2ecf8f 100644 --- a/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapter.java +++ b/extensions/exquery/restxq/src/main/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapter.java @@ -26,20 +26,26 @@ */ package org.exist.extensions.exquery.restxq.impl.adapters; -import net.sf.cglib.proxy.Callback; -import net.sf.cglib.proxy.CallbackFilter; -import net.sf.cglib.proxy.Dispatcher; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.NoOp; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.matcher.ElementMatchers; import org.exist.dom.persistent.NodeHandle; import org.exist.dom.persistent.NodeProxy; +import org.exist.xquery.Expression; import org.exist.xquery.value.Type; import org.w3c.dom.Attr; +import org.w3c.dom.Comment; +import org.w3c.dom.CDATASection; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; +import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; +import java.lang.reflect.InvocationTargetException; + + /** * A NodeProxy Proxy which enhances NodeProxy * with a W3C DOM implementation by proxying to @@ -49,103 +55,49 @@ * @author Adam Retter */ class DomEnhancingNodeProxyAdapter { - + public static NodeProxy create(final NodeProxy nodeProxy) { - - final Class clazzes[] = getNodeClasses(nodeProxy); - - // NoOp Callback is for NodeProxy calls - // NodeDispatched Callback is for the underlying Node calls - final Callback[] callbacks = { - NoOp.INSTANCE, - new NodeDispatcher(nodeProxy) - }; - - final CallbackFilter callbackFilter = method -> { - final Class declaringClass = method.getDeclaringClass(); + final Class domClazz = getNodeClass(nodeProxy); - //look for nodes - boolean isMethodOnNode = false; - if(declaringClass.equals(Node.class)) { - isMethodOnNode = true; - } else { - //search parent interfaces - for(final Class iface : declaringClass.getInterfaces()) { - if(iface.equals(Node.class)) { - isMethodOnNode = true; - break; - } - } - } + final DynamicType.Builder byteBuddyBuilder = new ByteBuddy() + .subclass(NodeProxy.class) + .implement(domClazz) - if(isMethodOnNode) { - return 1; //The NodeDispatcher - } else { - return 0; //The NoOp pass through - } - }; - - final Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(NodeProxy.class); - enhancer.setInterfaces(clazzes); - enhancer.setCallbackFilter(callbackFilter); - enhancer.setCallbacks(callbacks); - - return (NodeProxy)enhancer.create( - new Class[] { - NodeHandle.class - }, - new Object[] { - nodeProxy - }); - } - - private static Class[] getNodeClasses(final NodeProxy nodeProxy) { - switch(nodeProxy.getType()) { - - case Type.DOCUMENT: - return new Class[] { - Document.class, - Node.class - }; - - case Type.ELEMENT: - return new Class[] { - Element.class, - Node.class - }; + .method(ElementMatchers.isDeclaredBy(NodeProxy.class)) + .intercept(MethodCall.invokeSelf().on(nodeProxy).withAllArguments()) - case Type.ATTRIBUTE: - return new Class[] { - Attr.class, - Node.class - }; - - case Type.TEXT: - return new Class[] { - Text.class, - Node.class - }; + .method(ElementMatchers.isDeclaredBy(domClazz).or(ElementMatchers.isDeclaredBy(Node.class))) + .intercept(MethodCall.invokeSelf().on(nodeProxy.getNode()).withAllArguments()) - default: - return new Class[] { - Node.class - }; + .method(ElementMatchers.isHashCode()) + .intercept(MethodCall.invokeSelf().on(nodeProxy)); + + try { + final NodeProxy nodeProxyProxy = byteBuddyBuilder + .make() + .load(nodeProxy.getClass().getClassLoader()) + .getLoaded() + .getDeclaredConstructor(Expression.class, NodeHandle.class) + .newInstance(nodeProxy.getExpression(), nodeProxy); + + return nodeProxyProxy; + } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new IllegalStateException(e.getMessage(), e); } } - - public static class NodeDispatcher implements Dispatcher { - - private final NodeProxy nodeProxy; - - public NodeDispatcher(final NodeProxy nodeProxy) { - this.nodeProxy = nodeProxy; - } - - @Override - public Object loadObject() throws Exception { - return nodeProxy.getNode(); - } + + private static Class getNodeClass(final NodeProxy nodeProxy) { + return switch (nodeProxy.getType()) { + case Type.ELEMENT -> Element.class; + case Type.ATTRIBUTE -> Attr.class; + case Type.TEXT -> Text.class; + case Type.PROCESSING_INSTRUCTION -> ProcessingInstruction.class; + case Type.COMMENT -> Comment.class; + case Type.DOCUMENT -> Document.class; + case Type.CDATA_SECTION -> CDATASection.class; + default -> Node.class; + }; } -} \ No newline at end of file +} diff --git a/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapterTest.java b/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapterTest.java new file mode 100644 index 00000000000..c27ace73466 --- /dev/null +++ b/extensions/exquery/restxq/src/test/java/org/exist/extensions/exquery/restxq/impl/adapters/DomEnhancingNodeProxyAdapterTest.java @@ -0,0 +1,195 @@ +/* + * Copyright © 2001, Adam Retter + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.exist.extensions.exquery.restxq.impl.adapters; + +import com.evolvedbinary.j8fu.function.ConsumerE; +import org.exist.EXistException; +import org.exist.collections.Collection; +import org.exist.dom.persistent.*; +import org.exist.numbering.DLN; +import org.exist.numbering.NodeId; +import org.exist.security.PermissionDeniedException; +import org.exist.storage.BrokerPool; +import org.exist.storage.DBBroker; +import org.exist.storage.lock.Lock; +import org.exist.storage.txn.Txn; +import org.exist.test.ExistEmbeddedServer; +import org.exist.util.LockException; +import org.exist.util.MimeType; +import org.exist.util.StringInputSource; +import org.exist.xmldb.XmldbURI; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.Assert.*; + +/** + * @author Adam Retter + */ +public class DomEnhancingNodeProxyAdapterTest { + + @ClassRule + public static final ExistEmbeddedServer existEmbeddedServer = new ExistEmbeddedServer(true, true); + + private static final XmldbURI TEST_COLLECTION_URI = XmldbURI.DB.append("dom-enhancing-test"); + private static final XmldbURI TEST_DOC_URI = XmldbURI.create("test-doc.xml"); + + private static final String TEST_DOC = """ + + text1 + + + + """; + + @BeforeClass + public static void setup() throws EXistException, PermissionDeniedException, IOException, SAXException, LockException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + try (final Txn transaction = brokerPool.getTransactionManager().beginTransaction(); + final DBBroker broker = brokerPool.get(Optional.of(brokerPool.getSecurityManager().getSystemSubject())); + final Collection collection = broker.getOrCreateCollection(transaction, TEST_COLLECTION_URI)) { + + collection.storeDocument(transaction, broker, TEST_DOC_URI, new StringInputSource(TEST_DOC), MimeType.XML_TYPE); + + // async release of collection lock + collection.close(); + + transaction.commit(); + } + } + + + @Test + public void asElement() throws PermissionDeniedException, EXistException { + final NodeId elementId1 = new DLN("1"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, elementId1, Element.class, ElementImpl.class, elementProxy -> + assertEquals("test-doc", elementProxy.getNodeName()) + ) + ); + + final NodeId elementId2 = new DLN("1.2"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, elementId2, Element.class, ElementImpl.class, elementProxy -> + assertEquals("element1", elementProxy.getNodeName()) + ) + ); + } + + @Test + public void asAttr() throws PermissionDeniedException, EXistException { + final NodeId attrId = new DLN("1.2.1"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, attrId, Attr.class, AttrImpl.class, attrProxy -> + assertEquals("attr1", attrProxy.getNodeName()) + ) + ); + } + + @Test + public void asText() throws PermissionDeniedException, EXistException { + final NodeId textId = new DLN("1.2.2"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, textId, Text.class, TextImpl.class, textProxy -> + assertEquals("text1", textProxy.getTextContent()) + ) + ); + } + + @Test + public void asComment() throws PermissionDeniedException, EXistException { + final NodeId commentId = new DLN("1.4"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, commentId, Comment.class, CommentImpl.class, commentProxy -> + assertEquals("comment1", commentProxy.getTextContent()) + ) + ); + } + + @Test + public void asCdataSection() throws PermissionDeniedException, EXistException { + final DLN cdataSectionId = new DLN("1.6"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, cdataSectionId, CDATASection.class, CDATASectionImpl.class, cdataSectionProxy -> + assertEquals("cdata1", cdataSectionProxy.getTextContent()) + ) + ); + } + + @Test + public void asProcessingInstruction() throws PermissionDeniedException, EXistException { + final DLN processingInstructionId = new DLN("1.8"); + withTestDocument(doc -> + assertProxiedCorrectly(doc, processingInstructionId, ProcessingInstruction.class, ProcessingInstructionImpl.class, processingInstructionProxy -> + assertEquals("pi1", processingInstructionProxy.getData()) + ) + ); + } + + private static void withTestDocument(final ConsumerE fnDocument) throws AssertionError, EXistException, PermissionDeniedException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + try (final Txn transaction = brokerPool.getTransactionManager().beginTransaction(); + final DBBroker broker = brokerPool.get(Optional.of(brokerPool.getSecurityManager().getSystemSubject())); + final LockedDocument lockedDocument = broker.getXMLResource(TEST_COLLECTION_URI.append(TEST_DOC_URI), Lock.LockMode.READ_LOCK)) { + + final DocumentImpl doc = lockedDocument.getDocument(); + + fnDocument.accept(doc); + + transaction.commit(); + } + } + + private static
void assertProxiedCorrectly(final DocumentImpl doc, final NodeId nodeId, final Class
domInterfaceType, final Class existDomImplementationType, final ConsumerE proxiedDomTypeAssertions) throws AssertionError { + final NodeProxy nodeProxy = new NodeProxy(doc, nodeId); + nodeProxy.getNode(); // NOTE(AR) causes type of the node proxy to be set + + // check type of original + assertTrue("Expected instanceof NodeProxy", nodeProxy instanceof NodeProxy); + assertFalse("Expected not(instanceof " + domInterfaceType.getSimpleName() + ")", domInterfaceType.isInstance(nodeProxy)); + + // the function under test + final NodeProxy nodeProxyProxy = DomEnhancingNodeProxyAdapter.create(nodeProxy); + + // check type of proxy + assertTrue("Expected instanceof NodeProxy", nodeProxyProxy instanceof NodeProxy); + assertTrue("Expected instanceof " + domInterfaceType.getSimpleName() + "; W3C Node type was: " + nodeProxyProxy.getNodeType(), domInterfaceType.isInstance(nodeProxyProxy)); + + // check W3C DOM Interface methods of proxy + proxiedDomTypeAssertions.accept((DT) nodeProxyProxy); + + // check eXist-db NodeProxy methods of proxy + assertEquals(nodeId, nodeProxyProxy.getNodeId()); + assertTrue("Expected instanceof " + existDomImplementationType.getSimpleName(), existDomImplementationType.isInstance(nodeProxyProxy.getNode())); + } +}