From 9e0b513c21324d79e969b93cb83796310a4f4130 Mon Sep 17 00:00:00 2001 From: Wagner Michael Date: Fri, 8 Feb 2019 09:37:39 +0100 Subject: [PATCH 1/2] DOMHandle: Reuse DocumentBuilder and LsParser Creating a new DocumentBuilder and LsParser each time DOMHandle#receiveContent gets called is very time consuming. Fix this by using a per thread cached DocumentBuilder in DomHandle. LSParser is created the first time receiveContent is called and then reused. Extracted CachedInstancePerThreadSupplier to be able to reuse it in DocumentBuilderFactories. Fixes #1054. Signed-off-by: Wagner Michael --- .../impl/CachedInstancePerThreadSupplier.java | 81 ++++++++++++++ .../client/impl/DocumentBuilderFactories.java | 60 ++++++++++ .../marklogic/client/impl/XmlFactories.java | 104 ++---------------- .../com/marklogic/client/io/DOMHandle.java | 59 +++------- 4 files changed, 164 insertions(+), 140 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadSupplier.java create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadSupplier.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadSupplier.java new file mode 100644 index 000000000..e55031a5f --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadSupplier.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2018 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client.impl; + +import java.lang.ref.SoftReference; +import java.util.function.Supplier; + +/** + * A supplier that caches results per thread. + *

+ * The supplier is thread safe. + *

+ * Upon first invocation from a certain thread it is guaranteed to invoke the {@code supplier}'s {@code get()} + * method to obtain a thread-specific result. + *

+ * Cached values are wrapped in a {@link SoftReference} to allow them to be garbage collected upon low + * memory. This may lead to multiple calls to the {@code delegate}'s {@code get()} method over the lifetime of a + * certain thread if a previous result was cleared due to low memory. + * + * @param the supplier's value type + */ +class CachedInstancePerThreadSupplier implements Supplier { + + private final ThreadLocal> cachedInstances = new ThreadLocal<>(); + + /** + * The underlying supplier, invoked to originally retrieve the per-thread result + */ + private final Supplier delegate; + + CachedInstancePerThreadSupplier(Supplier delegate) { + this.delegate = delegate; + + if (null == delegate) { + throw new IllegalArgumentException("Delegate must not be null"); + } + } + + /** + * Returns the thread-specific instance, possibly creating a new one if there is none exists. + * + * @return a thread specific instance of {@code }. Never {@literal null}. + */ + @Override + public T get() { + + SoftReference cachedInstanceReference = cachedInstances.get(); + + // careful, either the reference itself may be null (upon first access from a thread), or the referred-to + // instance may be null (after a GC run that cleared it out) + T cachedInstance = (null != cachedInstanceReference) ? cachedInstanceReference.get() : null; + + if (null == cachedInstance) { + // no instance for the current thread, create a new one ... + cachedInstance = delegate.get(); + if (null == cachedInstance) { + throw new IllegalStateException("Must not return null from " + delegate.getClass().getName() + + "::get() (" + delegate + ")"); + } + + // ... and retain it for later re-use + cachedInstances.set(new SoftReference<>(cachedInstance)); + } + + return cachedInstance; + } + +} \ No newline at end of file diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java new file mode 100644 index 000000000..e9f791628 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2018 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client.impl; + +import com.marklogic.client.MarkLogicInternalException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +public final class DocumentBuilderFactories { + + private static final DocumentBuilderFactory documentBuilderFactory = makeDocumentBuilderFactory(); + private static final CachedInstancePerThreadSupplier cachedDocumentBuilder = + new CachedInstancePerThreadSupplier<>(DocumentBuilderFactories::makeDocumentBuilder); + + private DocumentBuilderFactories() { + } // preventing instances of utility class + + private static DocumentBuilderFactory makeDocumentBuilderFactory() { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + + return factory; + } + + private static DocumentBuilder makeDocumentBuilder() { + try { + return documentBuilderFactory.newDocumentBuilder(); + } catch (final ParserConfigurationException e) { + throw new MarkLogicInternalException("Failed to create a document builder.", e); + } + } + + /** + * Returns a shared {@link DocumentBuilder}. This DocumentBuilder is created namespace-aware and not validating. + *

+ * Creating a DocumentBuilder is potentially a pretty expensive operation. Using a shared instance helps to amortize + * this initialization cost via reuse. + * + * @return a namespace-aware, non-validating {@link DocumentBuilder} + */ + public static DocumentBuilder getNamespaceAwareNotValidating() { + return cachedDocumentBuilder.get(); + } +} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java index 8e3806a38..fbc891220 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java @@ -17,19 +17,14 @@ import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLOutputFactory; -import java.lang.ref.SoftReference; public final class XmlFactories { private static final CachedInstancePerThreadSupplier cachedOutputFactory = - new CachedInstancePerThreadSupplier(new Supplier() { - @Override - public XMLOutputFactory get() { - return makeNewOutputFactory(); - } - }); + new CachedInstancePerThreadSupplier<>(XmlFactories::makeNewOutputFactory); - private XmlFactories() {} // preventing instances of utility class + private XmlFactories() { + } // preventing instances of utility class /** * Returns a new {@link XMLOutputFactory}. This factory will have its @@ -38,10 +33,8 @@ private XmlFactories() {} // preventing instances of utility class * CAUTION: Creating XML factories is potentially a pretty expensive operation. If possible, consider using a shared * instance ({@link #getOutputFactory()}) to amortize this initialization cost via reuse. * - * @return a namespace-repairing {@link XMLOutputFactory} - * - * @throws FactoryConfigurationError see {@link XMLOutputFactory#newInstance()} - * + * @return a namespace-repairing {@link XMLOutputFactory} + * @throws FactoryConfigurationError see {@link XMLOutputFactory#newInstance()} * @see #getOutputFactory() */ public static XMLOutputFactory makeNewOutputFactory() { @@ -57,94 +50,11 @@ public static XMLOutputFactory makeNewOutputFactory() { * Creating XML factories is potentially a pretty expensive operation. Using a shared instance helps to amortize * this initialization cost via reuse. * - * @return a namespace-repairing {@link XMLOutputFactory} - * - * @throws FactoryConfigurationError see {@link XMLOutputFactory#newInstance()} - * + * @return a namespace-repairing {@link XMLOutputFactory} + * @throws FactoryConfigurationError see {@link XMLOutputFactory#newInstance()} * @see #makeNewOutputFactory() if you really (really?) need an non-shared instance */ public static XMLOutputFactory getOutputFactory() { return cachedOutputFactory.get(); } - - /** - * Represents a supplier of results. - * - *

There is no requirement that a new or distinct result be returned each - * time the supplier is invoked. - * - * @param the type of results supplied by this supplier - */ - // TODO replace with java.util.function.Supplier after Java 8 migration - interface Supplier { - - /** - * Gets a result. - * - * @return a result - */ - T get(); - } - - /** - * A supplier that caches results per thread. - *

- * The supplier is thread safe. - *

- * Upon first invocation from a certain thread it is guaranteed to invoke the {@code supplier}'s {@code get()} - * method to obtain a thread-specific result. - *

- * Cached values are wrapped in a {@link java.lang.ref.SoftReference} to allow them to be garbage collected upon low - * memory. This may lead to multiple calls to the {@code delegate}'s {@code get()} method over the lifetime of a - * certain thread if a previous result was cleared due to low memory. - * - * @param the supplier's value type - */ - private static class CachedInstancePerThreadSupplier implements Supplier { - - private final ThreadLocal> cachedInstances = new ThreadLocal>(); - - /** - * The underlying supplier, invoked to originally retrieve the per-thread result - */ - private final Supplier delegate; - - CachedInstancePerThreadSupplier(Supplier delegate) { - this.delegate = delegate; - - if (null == delegate) { - throw new IllegalArgumentException("Delegate must not be null"); - } - } - - /** - * Returns the thread-specific instance, possibly creating a new one if there is none exists. - * - * @return a thread specific instance of {@code }. Never {@literal null}. - */ - @Override - public T get() { - - SoftReference cachedInstanceReference = cachedInstances.get(); - - // careful, either the reference itself may be null (upon first access from a thread), or the referred-to - // instance may be null (after a GC run that cleared it out) - T cachedInstance = (null != cachedInstanceReference) ? cachedInstanceReference.get() : null; - - if (null == cachedInstance) { - // no instance for the current thread, create a new one ... - cachedInstance = delegate.get(); - if (null == cachedInstance) { - throw new IllegalStateException("Must not return null from " + delegate.getClass().getName() - + "::get() (" + delegate + ")"); - } - - // ... and retain it for later re-use - cachedInstances.set(new SoftReference(cachedInstance)); - } - - return cachedInstance; - } - - } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java b/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java index 69d294588..3208dcb4c 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java @@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException; import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; @@ -31,6 +32,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import com.marklogic.client.impl.DocumentBuilderFactories; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.DOMException; @@ -68,7 +70,8 @@ public class DOMHandle private LSResourceResolver resolver; private Document content; - private DocumentBuilderFactory factory; + private DocumentBuilder documentBuilder = DocumentBuilderFactories.getNamespaceAwareNotValidating(); + private LSParser parser; private XPath xpathProcessor; /** @@ -208,30 +211,17 @@ public String toString() { } } - /** - * Returns the factory for building DOM documents. - * @return the document factory - * @throws ParserConfigurationException if it occurs while initializing the new factory - */ - public DocumentBuilderFactory getFactory() throws ParserConfigurationException { - if (factory == null) - factory = makeDocumentBuilderFactory(); - return factory; - } /** * Specifies the factory for building DOM documents. * @param factory the document factory */ - public void setFactory(DocumentBuilderFactory factory) { - this.factory = factory; - } - protected DocumentBuilderFactory makeDocumentBuilderFactory() throws ParserConfigurationException { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - factory.setValidating(false); - // TODO: XInclude - - return factory; + public void setFactory(final DocumentBuilderFactory factory) { + try { + this.documentBuilder = factory.newDocumentBuilder(); + this.parser = null; + } catch (final ParserConfigurationException e) { + throw new MarkLogicInternalException("Failed to make document builder.", e); + } } /** @@ -385,14 +375,11 @@ protected void receiveContent(InputStream content) { if (logger.isDebugEnabled()) logger.debug("Parsing DOM document from input stream"); - DocumentBuilderFactory factory = getFactory(); - if (factory == null) { - throw new MarkLogicInternalException("Failed to make DOM document builder factory"); + final DOMImplementationLS domImpl = (DOMImplementationLS) documentBuilder.getDOMImplementation(); + if (this.parser == null) { + parser = domImpl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null); } - DOMImplementationLS domImpl = (DOMImplementationLS) factory.newDocumentBuilder().getDOMImplementation(); - - LSParser parser = domImpl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null); if (resolver != null) { parser.getDomConfig().setParameter("resource-resolver", resolver); } @@ -402,9 +389,6 @@ protected void receiveContent(InputStream content) { domInput.setByteStream(content); this.content = parser.parse(domInput); - } catch (ParserConfigurationException e) { - logger.error("Failed to parse DOM document from input stream",e); - throw new MarkLogicInternalException(e); } finally { try { content.close(); @@ -428,23 +412,12 @@ public void write(OutputStream out) throws IOException { if (logger.isDebugEnabled()) logger.debug("Serializing DOM document to output stream"); - DocumentBuilderFactory factory = getFactory(); - if (factory == null) { - throw new MarkLogicInternalException("Failed to make DOM document builder factory"); - } - - DOMImplementationLS domImpl = (DOMImplementationLS) factory.newDocumentBuilder().getDOMImplementation(); + DOMImplementationLS domImpl = (DOMImplementationLS) documentBuilder.getDOMImplementation(); LSOutput domOutput = domImpl.createLSOutput(); domOutput.setEncoding("UTF-8"); domOutput.setByteStream(out); domImpl.createLSSerializer().write(content, domOutput); - } catch (DOMException e) { - logger.error("Failed to serialize DOM document to output stream",e); - throw new MarkLogicInternalException(e); - } catch (LSException e) { - logger.error("Failed to serialize DOM document to output stream",e); - throw new MarkLogicInternalException(e); - } catch (ParserConfigurationException e) { + } catch (final DOMException | LSException e) { logger.error("Failed to serialize DOM document to output stream",e); throw new MarkLogicInternalException(e); } From 1be2176d11cea80c1ddada7b4089023148bf0f0e Mon Sep 17 00:00:00 2001 From: Wagner Michael Date: Mon, 11 Feb 2019 10:19:27 +0100 Subject: [PATCH 2/2] DOMHandle: Reuse cached LsParser * Moved DocumentBuilderFactories to existing XmlFactories. * Created class CachedInstancePerThreadFunction similar to CachedInstancePerThreadSupplier. * Re-using thread-local cached instances of LSParser in DOMHandle provided by XmlFactories. This results in faster receiveContent calls which do not re-use a DOMHandle, but rather use a own instance in each api call. Fixes #1054. Signed-off-by: Wagner Michael --- .../impl/CachedInstancePerThreadFunction.java | 110 ++++++++++++++++++ .../client/impl/DocumentBuilderFactories.java | 60 ---------- .../marklogic/client/impl/XmlFactories.java | 61 ++++++++++ .../com/marklogic/client/io/DOMHandle.java | 61 +++++----- 4 files changed, 202 insertions(+), 90 deletions(-) create mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadFunction.java delete mode 100644 marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadFunction.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadFunction.java new file mode 100644 index 000000000..7e31d9637 --- /dev/null +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/CachedInstancePerThreadFunction.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2018 MarkLogic Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.marklogic.client.impl; + +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +/** + * A {@link Function} that caches results per thread. + *

+ * The Function is thread safe. + *

+ * Upon first invocation from a certain thread it is guaranteed to invoke the {@link Function}'s {@link Function#apply(Object) apply} + * method to obtain a thread-specific result. + *

+ * Uses a Map internally to cache values created by the Function. The hashCode of the Function's arguments is used as a key for the map. + *

+ * Cached values are wrapped in a {@link SoftReference} to allow them to be garbage collected upon low + * memory. This may lead to multiple calls to the {@code delegate}'s {@link Function#apply(Object)} method over the lifetime of a + * certain thread if a previous result was cleared due to low memory. + * + * @param the delegate function's parameter type. + * @param the delegate function's value type. + */ +class CachedInstancePerThreadFunction implements Function { + + private final ThreadLocal>> cachedInstances = new ThreadLocal<>(); + + /** The underlying function, invoked to originally retrieve the per-thread result. */ + private final Function delegate; + + CachedInstancePerThreadFunction(final Function delegate) { + this.delegate = delegate; + + if (null == delegate) { + throw new IllegalArgumentException("Delegate must not be null."); + } + } + + /** + * Returns the thread-specific instance, possibly creating a new one if there is none exists. + * + * @param key value which is passed as an argument to the delegating function. Required to be not {@literal null}. + * @return a thread specific instance of {@code }. Never {@literal null}. + */ + @Override + public T apply(final E key) { + Objects.requireNonNull(key); + + T result; + final SoftReference> cachedInstanceReference = cachedInstances.get(); + + // careful, either the reference itself may be null (upon first access from a thread), or the referred-to + // instance may be null (after a GC run that cleared it out) + Map cacheMap = (null != cachedInstanceReference) ? cachedInstanceReference.get() : null; + + if (null == cacheMap) { + // no map cache instance for the current thread, create a new one ... + cacheMap = new HashMap<>(); + result = get(key); + cacheMap.put(key.hashCode(), result); + + // ... and retain it for later re-use + cachedInstances.set(new SoftReference<>(cacheMap)); + } else { + // cache map already existing + result = cacheMap.get(key.hashCode()); + + if (null == result) { + // no cached instance found in the threads cache map + // use delegate to create a new one and cache it + result = get(key); + cacheMap.put(key.hashCode(), result); + } + } + + return result; + } + + /** + * Creates a instance using the provided {@link #delegate}. + * + * @param key key for the delegating function. Expected to be not {@literal null}. + * @return Result of the delegating function. Never {@literal null}. + */ + private T get(final E key) { + final T result = delegate.apply(key); + if (null == result) { + throw new IllegalStateException(String.format("Must not return null from %s::apply() (%s)", delegate.getClass().getName(), delegate)); + } + + return result; + } +} \ No newline at end of file diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java deleted file mode 100644 index e9f791628..000000000 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/DocumentBuilderFactories.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012-2018 MarkLogic Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.marklogic.client.impl; - -import com.marklogic.client.MarkLogicInternalException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -public final class DocumentBuilderFactories { - - private static final DocumentBuilderFactory documentBuilderFactory = makeDocumentBuilderFactory(); - private static final CachedInstancePerThreadSupplier cachedDocumentBuilder = - new CachedInstancePerThreadSupplier<>(DocumentBuilderFactories::makeDocumentBuilder); - - private DocumentBuilderFactories() { - } // preventing instances of utility class - - private static DocumentBuilderFactory makeDocumentBuilderFactory() { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); - factory.setValidating(false); - - return factory; - } - - private static DocumentBuilder makeDocumentBuilder() { - try { - return documentBuilderFactory.newDocumentBuilder(); - } catch (final ParserConfigurationException e) { - throw new MarkLogicInternalException("Failed to create a document builder.", e); - } - } - - /** - * Returns a shared {@link DocumentBuilder}. This DocumentBuilder is created namespace-aware and not validating. - *

- * Creating a DocumentBuilder is potentially a pretty expensive operation. Using a shared instance helps to amortize - * this initialization cost via reuse. - * - * @return a namespace-aware, non-validating {@link DocumentBuilder} - */ - public static DocumentBuilder getNamespaceAwareNotValidating() { - return cachedDocumentBuilder.get(); - } -} diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java b/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java index fbc891220..6e045ce51 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/impl/XmlFactories.java @@ -15,6 +15,13 @@ */ package com.marklogic.client.impl; +import com.marklogic.client.MarkLogicInternalException; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSParser; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLOutputFactory; @@ -23,6 +30,13 @@ public final class XmlFactories { private static final CachedInstancePerThreadSupplier cachedOutputFactory = new CachedInstancePerThreadSupplier<>(XmlFactories::makeNewOutputFactory); + private static final DocumentBuilderFactory documentBuilderFactory = makeDocumentBuilderFactory(); + private static final CachedInstancePerThreadSupplier cachedDocumentBuilder = + new CachedInstancePerThreadSupplier<>(XmlFactories::makeDocumentBuilder); + + private static final CachedInstancePerThreadFunction cachedLsParser = + new CachedInstancePerThreadFunction<>(XmlFactories::makeLSParser); + private XmlFactories() { } // preventing instances of utility class @@ -57,4 +71,51 @@ public static XMLOutputFactory makeNewOutputFactory() { public static XMLOutputFactory getOutputFactory() { return cachedOutputFactory.get(); } + + private static DocumentBuilderFactory makeDocumentBuilderFactory() { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + + return factory; + } + + private static DocumentBuilder makeDocumentBuilder() { + try { + return documentBuilderFactory.newDocumentBuilder(); + } catch (final ParserConfigurationException e) { + throw new MarkLogicInternalException("Failed to create a document builder.", e); + } + } + + /** + * Returns a shared {@link DocumentBuilder}. This DocumentBuilder is created namespace-aware and not validating. + *

+ * Creating a DocumentBuilder is potentially a pretty expensive operation. Using a shared instance helps to amortize + * this initialization cost via reuse. + * + * @return a namespace-aware, non-validating {@link DocumentBuilder} + */ + public static DocumentBuilder getNsAwareNotValidatingDocBuilder() { + return cachedDocumentBuilder.get(); + } + + /** + * Returns a shared {@link LSParser}. This LSParser will have {@link DOMImplementationLS#MODE_SYNCHRONOUS} and no {@code schemaType} set. + *

+ * Every first usage per {@code documentBuilder} instance will create a new {@link LSParser}. + * Further usages might return a cached LSParser instance. + * + * @param documentBuilder DocumentBuilder from which the LSParser is created. + * @return a synchronous LSParser + */ + public static LSParser getSynchronousLSParser(final DocumentBuilder documentBuilder) { + return cachedLsParser.apply(documentBuilder); + } + + private static LSParser makeLSParser(final DocumentBuilder documentBuilder) { + final DOMImplementationLS domImpl = (DOMImplementationLS) documentBuilder.getDOMImplementation(); + + return domImpl.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null); + } } diff --git a/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java b/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java index 3208dcb4c..bf902903a 100644 --- a/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java +++ b/marklogic-client-api/src/main/java/com/marklogic/client/io/DOMHandle.java @@ -15,24 +15,17 @@ */ package com.marklogic.client.io; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; - -import javax.xml.namespace.QName; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; - -import com.marklogic.client.impl.DocumentBuilderFactories; +import com.marklogic.client.MarkLogicIOException; +import com.marklogic.client.MarkLogicInternalException; +import com.marklogic.client.impl.XmlFactories; +import com.marklogic.client.io.marker.BufferableHandle; +import com.marklogic.client.io.marker.ContentHandle; +import com.marklogic.client.io.marker.ContentHandleFactory; +import com.marklogic.client.io.marker.CtsQueryWriteHandle; +import com.marklogic.client.io.marker.StructureReadHandle; +import com.marklogic.client.io.marker.StructureWriteHandle; +import com.marklogic.client.io.marker.XMLReadHandle; +import com.marklogic.client.io.marker.XMLWriteHandle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.DOMException; @@ -46,16 +39,21 @@ import org.w3c.dom.ls.LSParser; import org.w3c.dom.ls.LSResourceResolver; -import com.marklogic.client.MarkLogicIOException; -import com.marklogic.client.MarkLogicInternalException; -import com.marklogic.client.io.marker.BufferableHandle; -import com.marklogic.client.io.marker.ContentHandle; -import com.marklogic.client.io.marker.ContentHandleFactory; -import com.marklogic.client.io.marker.CtsQueryWriteHandle; -import com.marklogic.client.io.marker.StructureReadHandle; -import com.marklogic.client.io.marker.StructureWriteHandle; -import com.marklogic.client.io.marker.XMLReadHandle; -import com.marklogic.client.io.marker.XMLWriteHandle; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; /** * A DOM Handle represents XML content as a DOM document for reading or writing. @@ -70,8 +68,8 @@ public class DOMHandle private LSResourceResolver resolver; private Document content; - private DocumentBuilder documentBuilder = DocumentBuilderFactories.getNamespaceAwareNotValidating(); - private LSParser parser; + private DocumentBuilder documentBuilder = XmlFactories.getNsAwareNotValidatingDocBuilder(); + private LSParser parser = XmlFactories.getSynchronousLSParser(documentBuilder); private XPath xpathProcessor; /** @@ -128,6 +126,9 @@ public LSResourceResolver getResolver() { */ public void setResolver(LSResourceResolver resolver) { this.resolver = resolver; + + // remove possibly cached LSParser + this.parser = null; } /**