/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2015 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.exist.xmldb;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.exist.dom.persistent.NodeProxy;
import org.exist.dom.persistent.XMLUtil;
import org.exist.dom.memtree.AttrImpl;
import org.exist.dom.memtree.NodeImpl;
import org.exist.numbering.NodeId;
import org.exist.security.Subject;
import org.exist.storage.BrokerPool;
import org.exist.storage.serializers.Serializer;
import org.exist.util.MimeType;
import org.exist.util.serializer.DOMSerializer;
import org.exist.util.serializer.DOMStreamer;
import org.exist.util.serializer.SAXSerializer;
import org.exist.util.serializer.SerializerPool;
import org.exist.xquery.XPathException;
import org.exist.xquery.value.AtomicValue;
import org.exist.xquery.value.NodeValue;
import org.exist.xquery.value.StringValue;
import org.exist.xquery.value.Type;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.*;
import org.xml.sax.ext.LexicalHandler;
import org.xmldb.api.base.ErrorCodes;
import org.xmldb.api.base.XMLDBException;
import org.xmldb.api.modules.XMLResource;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Properties;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Local implementation of XMLResource.
*/
public class LocalXMLResource extends AbstractEXistResource implements XMLResource {
private NodeProxy proxy = null;
private Properties outputProperties;
private LexicalHandler lexicalHandler = null;
// those are the different types of content this resource
// may have to deal with
protected String content = null;
protected Path file = null;
protected InputSource inputSource = null;
protected Node root = null;
protected AtomicValue value = null;
public LocalXMLResource(final Subject user, final BrokerPool brokerPool, final LocalCollection parent, final XmldbURI did) throws XMLDBException {
super(user, brokerPool, parent, did, MimeType.XML_TYPE.getName());
this.outputProperties = parent != null ? parent.getProperties() : null;
}
public LocalXMLResource(final Subject user, final BrokerPool brokerPool, final LocalCollection parent, final NodeProxy p) throws XMLDBException {
this(user, brokerPool, parent, p.getOwnerDocument().getFileURI());
this.proxy = p;
this.outputProperties = parent != null ? parent.getProperties() : null;
}
@Override
public String getDocumentId() throws XMLDBException {
return docId.toString();
}
@Override
public String getResourceType() throws XMLDBException {
return XMLResource.RESOURCE_TYPE;
}
@Override
public Object getContent() throws XMLDBException {
if (content != null) {
return content;
}
// Case 1: content is an external DOM node
else if (root != null && !(root instanceof NodeValue)) {
final StringWriter writer = new StringWriter();
final DOMSerializer serializer = new DOMSerializer(writer, getProperties());
try {
serializer.serialize(root);
content = writer.toString();
} catch (final TransformerException e) {
throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, e.getMessage(), e);
}
return content;
// Case 2: content is an atomic value
} else if (value != null) {
try {
if (Type.subTypeOf(value.getType(),Type.STRING)) {
return ((StringValue)value).getStringValue(true);
} else {
return value.getStringValue();
}
} catch (final XPathException e) {
throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, e.getMessage(), e);
}
// Case 3: content is a file
} else if (file != null) {
try {
content = XMLUtil.readFile(file);
return content;
} catch (final IOException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "error while reading resource contents", e);
}
// Case 4: content is an input source
} else if (inputSource != null) {
try {
content = XMLUtil.readFile(inputSource);
return content;
} catch (final IOException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "error while reading resource contents", e);
}
// Case 5: content is a document or internal node, we MUST serialize it
} else {
content = withDb((broker, transaction) -> {
final Serializer serializer = broker.newSerializer();
serializer.setUser(user);
try {
serializer.setProperties(getProperties());
if (root != null) {
return serializer.serialize((NodeValue) root);
} else if (proxy != null) {
return serializer.serialize(proxy);
} else {
return this.<String>read(broker, transaction).apply((document, broker1, transaction1) -> {
try {
return serializer.serialize(document);
} catch (final SAXException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
}
});
}
} catch (final SAXException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
}
});
return content;
}
}
@Override
public Node getContentAsDOM() throws XMLDBException {
final Node result;
if (root != null) {
if(root instanceof NodeImpl) {
withDb((broker, transaction) -> {
((NodeImpl)root).expand();
return null;
});
}
result = root;
} else if (value != null) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "cannot return an atomic value as DOM node");
} else {
result = read((document, broker, transaction) -> {
if (proxy != null) {
return document.getNode(proxy);
} else {
// <frederic.glorieux@ajlsm.com> return a full to get root PI and comments
return document;
}
});
}
return exportInternalNode(result);
}
/**
* Provides a safe export of an internal persistent DOM
* node from eXist via the Local XML:DB API.
*
* This is done by providing a proxy object that only implements
* the appropriate W3C DOM interface. This helps prevent the
* XML:DB Local API from leaking implementation through
* its abstractions.
*/
private Node exportInternalNode(final Node node) {
final Optional<Class<? extends Node>> domClazz = getW3cNodeInterface(node.getClass());
if(!domClazz.isPresent()) {
throw new IllegalArgumentException("Provided node does not implement org.w3c.dom");
}
final Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(domClazz.get());
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(final Object obj, final Method method, final Object[] args, final MethodProxy proxy) throws Throwable {
final Object domResult = method.invoke(node, args);
if(domResult != null && Node.class.isAssignableFrom(method.getReturnType())) {
return exportInternalNode((Node) domResult); //recursively wrap node result
} else if(domResult != null && method.getReturnType().equals(NodeList.class)) {
final NodeList underlying = (NodeList)domResult; //recursively wrap nodes in nodelist result
return new NodeList() {
@Override
public Node item(final int index) {
return Optional.ofNullable(underlying.item(index))
.map(n -> exportInternalNode(n))
.orElse(null);
}
@Override
public int getLength() {
return underlying.getLength();
}
};
} else {
return domResult;
}
}
});
return (Node)enhancer.create();
}
private Optional<Class<? extends Node>> getW3cNodeInterface(final Class<? extends Node> nodeClazz) {
return Stream.of(nodeClazz.getInterfaces())
.filter(iface -> iface.getPackage().getName().equals("org.w3c.dom"))
.findFirst()
.map(c -> (Class<? extends Node>)c);
}
@Override
public void getContentAsSAX(final ContentHandler handler) throws XMLDBException {
// case 1: content is an external DOM node
if (root != null && !(root instanceof NodeValue)) {
try {
final String option = collection.getProperty(Serializer.GENERATE_DOC_EVENTS, "false");
final DOMStreamer streamer = (DOMStreamer) SerializerPool.getInstance().borrowObject(DOMStreamer.class);
streamer.setContentHandler(handler);
streamer.setLexicalHandler(lexicalHandler);
streamer.serialize(root, option.equalsIgnoreCase("true"));
SerializerPool.getInstance().returnObject(streamer);
} catch (final Exception e) {
throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, e.getMessage(), e);
}
} else {
withDb((broker, transaction) -> {
try {
// case 2: content is an atomic value
if (value != null) {
value.toSAX(broker, handler, getProperties());
// case 3: content is an internal node or a document
} else {
final Serializer serializer = broker.newSerializer();
serializer.setUser(user);
serializer.setProperties(getProperties());
serializer.setSAXHandlers(handler, lexicalHandler);
if (root != null) {
serializer.toSAX((NodeValue) root);
} else if (proxy != null) {
serializer.toSAX(proxy);
} else {
read(broker, transaction).apply((document, broker1, transaction1) -> {
try {
serializer.toSAX(document);
return null;
} catch(final SAXException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
}
});
}
}
return null;
} catch(final SAXException e) {
throw new XMLDBException(ErrorCodes.VENDOR_ERROR, e.getMessage(), e);
}
});
}
}
/**
* Sets the content for this resource. If value is of type File, it is
* directly passed to the parser when Collection.storeResource is called.
* Otherwise the method tries to convert the value to String.
*
* Passing a File object should be preferred if the document is large. The
* file's content will not be loaded into memory but directly passed to a
* SAX parser.
*
* @param obj the content value to set for the resource.
* @exception XMLDBException with expected error codes. <br />
* <code>ErrorCodes.VENDOR_ERROR</code> for any vendor specific errors
* that occur. <br />
*/
@Override
public void setContent(final Object obj) throws XMLDBException {
content = null;
file = null;
value = null;
inputSource = null;
root = null;
if (obj instanceof Path) {
file = (Path) obj;
} else if (obj instanceof java.io.File) {
file = ((java.io.File) obj).toPath();
} else if (obj instanceof AtomicValue) {
value = (AtomicValue) obj;
} else if (obj instanceof InputSource) {
inputSource=(InputSource) obj;
} else if (obj instanceof byte[]) {
content = new String((byte[])obj, UTF_8);
} else {
content = obj.toString();
}
}
@Override
public void setContentAsDOM(final Node root) throws XMLDBException {
if (root instanceof AttrImpl) {
throw new XMLDBException(ErrorCodes.WRONG_CONTENT_TYPE, "SENR0001: can not serialize a standalone attribute");
}
content = null;
file = null;
value = null;
inputSource = null;
this.root = root;
}
@Override
public ContentHandler setContentAsSAX() throws XMLDBException {
file = null;
value = null;
inputSource = null;
root = null;
return new InternalXMLSerializer();
}
@Override
public void freeResources() throws XMLDBException {
//dO nothing
//TODO consider unifying close() code into freeResources()
}
@Override
public boolean getSAXFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
return false;
}
@Override
public void setSAXFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
}
@Override
public void setLexicalHandler(final LexicalHandler lexicalHandler) {
this.lexicalHandler = lexicalHandler;
}
public void setProperties(final Properties properties) {
this.outputProperties = properties;
}
public Properties getProperties() {
return outputProperties;
}
public NodeProxy getNode() throws XMLDBException {
if(proxy != null) {
return proxy;
} else {
return read((document, broker, transaction) -> new NodeProxy(document, NodeId.DOCUMENT_NODE));
}
}
@Override
public DocumentType getDocType() throws XMLDBException {
return read((document, broker, transaction) -> document.getDoctype());
}
@Override
public void setDocType(final DocumentType doctype) throws XMLDBException {
modify((document, broker, transaction) -> {
if (document == null) {
throw new XMLDBException(ErrorCodes.INVALID_RESOURCE, "Resource " + docId + " not found");
}
document.setDocumentType(doctype);
return null;
});
}
private class InternalXMLSerializer extends SAXSerializer {
public InternalXMLSerializer() {
super(new StringWriter(), null);
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
content = getWriter().toString();
}
}
}