com.marklogic.xcc.ContentFactory Maven / Gradle / Ivy
/*
* Copyright 2003-2019 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.xcc;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import com.fasterxml.jackson.databind.JsonNode;
import com.marklogic.xcc.types.XdmNode;
// Need an example of a user-implementation of the {@link Content}
// interface for sources like Web Services or SQL ResultSets.
/**
*
* A helper class for creating instances of {@link Content}.
*
*/
public class ContentFactory {
private ContentFactory() {
// This is a helper class, cannot be instantiated
}
/**
* Create a new {@link Content} object from a W3C DOM {@link Document} object. If not explicitly
* overridden, the document format will be set to XML. This factory method makes use of the
* standard JAX {@link TransformerFactory} facility to serialize the DOM to a UTF-8-encoded byte
* stream. See {@link javax.xml.transform.TransformerFactory#newInstance()} for information on
* how to customize the transformer implementation.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param document
* A W3C DOM {@link Document} object which is the content.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newContent(String uri, Document document, ContentCreateOptions createOptions) {
return newContent(uri, bytesFromW3cDoc(document), (createOptions == null) ? ContentCreateOptions
.newXmlInstance() : createOptions);
}
/**
* Create a new {@link Content} object from a W3C DOM {@link Node} object.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content. If
* not explicitly overridden, the document format will be set to XML. This factory
* method makes use of the standard JAX {@link TransformerFactory} facility to
* serialize the DOM to a UTF-8-encoded byte stream. See
* {@link javax.xml.transform.TransformerFactory#newInstance()} for information on
* how to customize the transformer implementation.
* @param documentNode
* A W3C DOM {@link Node} object which is the content. Only Nodes of type Element or
* Text are valid.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newContent(String uri, Node documentNode, ContentCreateOptions createOptions) {
return newContent(uri, bytesFromW3cNode(documentNode), (createOptions == null) ? ContentCreateOptions
.newXmlInstance() : createOptions);
}
/**
* Create a new {@link Content} object from a Jackson {@link Node} object.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content. If
* not explicitly overridden, the document format will be set to JSON.
* @param documentNode
* A Jackson {@link Node} object which is the content. Only Nodes of type Array or
* Object are valid.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newJsonContent(String uri, JsonNode documentNode, ContentCreateOptions createOptions) {
return newContent(uri, bytesFromString(documentNode.toString()), (createOptions == null) ? ContentCreateOptions
.newJsonInstance() : createOptions);
}
/**
* Create a new {@link Content} object from an {@link XdmNode}. This object may have been
* constructed locally or received as a member of a {@link ResultSequence}. If not explicitly
* overridden, the document format will be set to XML.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentNode
* An instance of {@link XdmNode} which is the document content. Only nodes of type
* {@link com.marklogic.xcc.types.XdmElement},
* {@link com.marklogic.xcc.types.XdmText} or
* {@link com.marklogic.xcc.types.XdmBinary} are valid.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newContent(String uri, XdmNode documentNode, ContentCreateOptions createOptions) {
return newContent(uri, bytesFromString(documentNode.asString()), (createOptions == null) ? ContentCreateOptions
.newXmlInstance() : createOptions);
}
/**
*
* Create a new {@link Content} object from a File object.
*
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentFile
* A File object from which the content will be read. If the createOptions argument
* selects {@link DocumentFormat#BINARY}, the content of the file will be transfered
* as an opaque blob. Otherwise, the file is assumed to be UTF-8 encoded text.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newContent(String uri, File documentFile, ContentCreateOptions createOptions) {
return new FileContent(uri, documentFile, createOptions);
}
/**
* Create a new {@link Content} object from a RandomAccessFile object.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentFile
* An open RandomAccessaFile object from which the content will be read. File data
* will be read from the current position to end-of-file. If the createOptions
* argument selects {@link DocumentFormat#BINARY}, the content of the file will be
* transfered as an opaque blob. Otherwise, the file is assumed to be UTF-8 encoded
* text.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws java.io.IOException
* If there is a problem reading data from the file.
*/
public static Content newContent(String uri, RandomAccessFile documentFile, ContentCreateOptions createOptions)
throws IOException {
return new RandomAccessFileContent(uri, documentFile, createOptions);
}
/**
* Create a new, non-rewindable {@link Content} object from a {@link URL}.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentUrl
* A {@link URL} object that represents the location from which the content can be
* fetched.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws IOException
* If there is a problem creating a {@link URL} or opening a data stream.
*/
public static Content newUnBufferedContent(String uri, URL documentUrl, ContentCreateOptions createOptions)
throws IOException {
return new InputStreamContent(uri, documentUrl.openStream(), createOptions);
}
/**
* Create a new, non-rewindable {@link Content} object from a {@link URI}.
*
* @param uri
* The {@link URI} (name) with which the document will be inserted into the content
* store. If the URI already exists in the store, it will be replaced with the new
* content.
* @param documentUri
* A URI object that represents the location from which the content can be fetched.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws IOException
* If there is a problem creating a {@link URL} or opening a data stream.
*/
public static Content newUnBufferedContent(String uri, URI documentUri, ContentCreateOptions createOptions)
throws IOException {
return newUnBufferedContent(uri, documentUri.toURL(), createOptions);
}
/**
* Create a new {@link Content} object from a URI, buffering so that it's rewindable and only
* accesses the URL one time.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentUrl
* A {@link URL} object that represents the location from which the content can be
* fetched.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws IOException
* If there is a problem creating a {@link URL} or reading the data from the stream.
*/
public static Content newContent(String uri, URL documentUrl, ContentCreateOptions createOptions)
throws IOException {
return new ByteArrayContent(uri, bytesFromStream(documentUrl.openStream()), createOptions);
}
/**
* Create a new {@link Content} object from a URI, buffering so that it's rewindable and only
* accesses the URL one time.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentUri
* A {@link URI} object that represents the location from which the content can be
* fetched. Unlike {@link URL}s, {@link URI} objects are not validated when they are
* created. This method converts the {@link URI} to a {@link URL}. If this parameter
* does not represent a valid URL, an exception will be thrown.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws IOException
* If there is a problem creating a {@link URL} or reading the data from the stream.
* @throws IllegalArgumentException
* If the URI is not absolute.
*/
public static Content newContent(String uri, URI documentUri, ContentCreateOptions createOptions)
throws IOException {
return newContent(uri, documentUri.toURL(), createOptions);
}
/**
* Create a new {@link Content} object from a String.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentString
* A String which will be stored as the content of the document. If the createOptions
* argument selects {@link DocumentFormat#BINARY}, the bytes representing the UTF-8
* encoding of the String will be stored.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @see DocumentFormat
*/
public static Content newContent(String uri, String documentString, ContentCreateOptions createOptions) {
return newContent(uri, bytesFromString(documentString), createOptions);
}
/**
* Create a new {@link Content} object from a byte array.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentBytes
* A byte array which will be stored as the content of the document. If the
* createOptions argument is null, then {@link DocumentFormat#BINARY} will be
* assumed.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @see DocumentFormat
*/
public static Content newContent(String uri, byte[] documentBytes, ContentCreateOptions createOptions) {
return new ByteArrayContent(uri, documentBytes, createOptions);
}
/**
* Create a new {@link Content} object from a subset of a byte array.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new content.
* @param documentBytes
* A byte array which will be stored as the content of the document. If the
* createOptions argument is null, then {@link DocumentFormat#BINARY} will be
* assumed.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @param offset
* The starting point of the content in the array.
* @param length
* The length of the content in the array.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @see DocumentFormat
* @since 3.2
*/
public static Content newContent(String uri, byte[] documentBytes, int offset, int length,
ContentCreateOptions createOptions) {
return new ByteArrayContent(uri, documentBytes, offset, length, createOptions);
}
// ---------------------------------------------------------------
/**
* Create a new {@link Content} object by consuming the given InputStream and buffereing it in
* memory. This factory method immediately reads the stream to the end and buffers it. This
* could result in an OutOfMemoryError if the stream contains a large amount of data. The
* provided documentStream will be closed.
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new value.
* @param documentStream
* The stream making up the document content.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
* @throws IOException
* If there is a problem reading the documentStream.
*/
public static Content newContent(String uri, InputStream documentStream, ContentCreateOptions createOptions)
throws IOException {
return new ByteArrayContent(uri, bytesFromStream(documentStream), createOptions);
}
/**
*
* Create a new non-rewindable {@link Content} object for the given InputStream. Note that the
* {@link Content} instance returned is not rewindable (
* {@link com.marklogic.xcc.Content#isRewindable()} == false) which means that auto-retry cannot
* be performed is there is a problem inserting the content.
*
*
* @param uri
* The URI (name) with which the document will be inserted into the content store. If
* the URI already exists in the store, it will be replaced with the new value.
* @param documentStream
* The stream making up the document content.
* @param createOptions
* Creation meta-information to be applied when the content is inserted into the
* contentbase. These options control the document format (json, xml, text, binary) and
* access permissions.
* @return A non-rewindable {@link Content} object suitable for passing to
* {@link Session#insertContent(Content)}
*/
public static Content newUnBufferedContent(String uri, InputStream documentStream,
ContentCreateOptions createOptions) {
return new InputStreamContent(uri, documentStream, createOptions);
}
// ---------------------------------------------------------------
private static byte[] bytesFromString(String string) {
try {
return string.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
return string.getBytes();
}
}
private static byte[] bytesFromStream(InputStream is) throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[64 * 1024];
int rc;
while ((rc = is.read(buffer)) != -1) {
os.write(buffer, 0, rc);
}
is.close();
os.flush();
buffer = os.toByteArray();
os.close();
return (buffer);
}
private static TransformerFactory transformerFactory = null;
private static synchronized TransformerFactory getTransformerFactory() {
if (transformerFactory == null) {
transformerFactory = TransformerFactory.newInstance();
}
return transformerFactory;
}
static byte[] bytesFromW3cNode(Node node) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Result rslt = new StreamResult(bos);
Source src = new DOMSource(node);
Transformer transformer;
try {
transformer = getTransformerFactory().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(src, rslt);
} catch (TransformerException e) {
throw new RuntimeException("Cannot serialize Node: " + e, e);
}
return bos.toByteArray();
}
static byte[] bytesFromW3cDoc(Document document) {
return (bytesFromW3cNode(document.getDocumentElement()));
}
// ---------------------------------------------------------------
// ---------------------------------------------------------------
private static class ByteArrayContent implements Content {
private final String uri;
private final byte[] bytes;
private final int offset;
private final int length;
private final ContentCreateOptions options;
public ByteArrayContent(String uri, byte[] bytes, int offset, int length, ContentCreateOptions options) {
if ((uri == null) || (uri.length() == 0)) {
throw new IllegalArgumentException("URI may not be null or zero-length");
}
this.uri = uri;
this.bytes = bytes;
this.offset = offset;
this.length = length;
this.options = options;
}
public ByteArrayContent(String uri, byte[] bytes, ContentCreateOptions options) {
this(uri, bytes, 0, bytes.length, options);
}
public String getUri() {
return (uri);
}
public InputStream openDataStream() {
return new ByteArrayInputStream(bytes, offset, length);
}
public ContentCreateOptions getCreateOptions() {
return options;
}
public boolean isRewindable() {
return true;
}
public void rewind() {
}
public long size() {
return length;
}
public void close() {
}
}
private static class FileContent implements Content {
private final String uri;
private final ContentCreateOptions options;
private final File file;
private InputStream activeStream;
public FileContent(String uri, File file, ContentCreateOptions options) {
if ((uri == null) || (uri.length() == 0)) {
throw new IllegalArgumentException("URI may not be null or zero-length");
}
this.uri = uri;
this.file = file;
this.options = options;
}
public String getUri() {
return uri;
}
public InputStream openDataStream() throws FileNotFoundException {
activeStream = new BufferedInputStream(new FileInputStream(file));
return activeStream;
}
public ContentCreateOptions getCreateOptions() {
return options;
}
public boolean isRewindable() {
return true;
}
public void rewind() throws IOException {
close();
}
public long size() {
return file.length();
}
public void close() {
if (activeStream != null) {
try {
activeStream.close();
} catch (IOException e) {
// don't care
}
activeStream = null;
}
}
}
private static class RandomAccessFileContent implements Content {
private final String uri;
private final ContentCreateOptions options;
private final RandomAccessFile raFile;
private final long start;
public RandomAccessFileContent(String uri, RandomAccessFile raFile, ContentCreateOptions options)
throws IOException {
if ((uri == null) || (uri.length() == 0)) {
throw new IllegalArgumentException("URI may not be null or zero-length");
}
this.uri = uri;
this.raFile = raFile;
this.options = options;
start = raFile.getFilePointer();
}
public String getUri() {
return uri;
}
public InputStream openDataStream() throws IOException {
raFile.seek(start);
return Channels.newInputStream(raFile.getChannel());
}
public ContentCreateOptions getCreateOptions() {
return options;
}
public boolean isRewindable() {
return true;
}
public void rewind() throws IOException {
raFile.seek(start);
}
public long size() {
try {
return raFile.length() - start;
} catch (IOException e) {
return -1;
}
}
public void close() {
try {
raFile.close();
} catch (IOException e) {
// smother it, we don't care
}
}
}
private static class InputStreamContent implements Content {
private final String uri;
private final ContentCreateOptions options;
private InputStream is;
public InputStreamContent(String uri, InputStream is, ContentCreateOptions options) {
this.uri = uri;
this.is = is;
this.options = options;
}
public String getUri() {
return uri;
}
public InputStream openDataStream() throws FileNotFoundException {
if (is == null) {
throw new IllegalStateException("Data stream has already been consumed");
}
InputStream isTmp = is;
is = null;
return isTmp;
}
public ContentCreateOptions getCreateOptions() {
return options;
}
public boolean isRewindable() {
return false;
}
public void rewind() throws IOException {
if (is == null) {
throw new IllegalStateException("This Content is not rewindable");
}
}
public long size() {
return -1;
}
public void close() {
is = null;
}
}
}