package org.exist.fluent;
import java.io.*;
import java.util.*;
import org.exist.dom.*;
import org.exist.security.Permission;
import org.exist.storage.DBBroker;
import org.exist.xquery.value.Sequence;
/**
* A document from the database, either binary or XML. Note that querying a non-XML
* document is harmless, but will never return any results.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public class Document extends NamedResource {
/**
* Listener for events affecting documents. The three possible actions are document
* creation, update (modification), and deletion.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public interface Listener extends org.exist.fluent.Listener {
/**
* Respond to a document event.
*
* @param ev the details of the event
*/
void handle(Document.Event ev);
}
/**
* An event that concerns a document.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public static class Event extends org.exist.fluent.Listener.Event {
/**
* The document that's the subject of this event.
* Note that for some timing/action combinations, this field might be <code>null</code>.
*/
public final Document document;
Event(Trigger trigger, String path, Document document) {
super(trigger, path);
this.document = document;
}
Event(ListenerManager.EventKey key, Document document) {
super(key);
this.document = document;
}
@Override
public boolean equals(Object o) {
return
super.equals(o) &&
(document == null ? ((Event) o).document == null : document.equals(((Event) o).document));
}
@Override
public int hashCode() {
return super.hashCode() * 37 + (document == null ? 0 : document.hashCode());
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(super.toString());
buf.insert(3, "Document.");
buf.insert(buf.length()-1, ", " + document);
return buf.toString();
}
}
/**
* The facet that gives access to a document's listeners.
*
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public class ListenersFacet {
/**
* Add a listener for this document. Equivalent to <code>add(EnumSet.of(trigger), listener)</code>.
*
* @see #add(Set, Document.Listener)
* @param trigger the kind of event the listener should be notified of
* @param listener the listener to notify of events
*/
public void add(Trigger trigger, Document.Listener listener) {
add(EnumSet.of(trigger), listener);
}
/**
* Add a listener for this document.
*
* @param triggers the kinds of events the listener should be notified of; the set must not be empty
* @param listener the listener to notify of events
*/
public void add(Set<Trigger> triggers, Document.Listener listener) {
staleMarker.check();
ListenerManager.INSTANCE.add(path(), ListenerManager.Depth.ZERO, triggers, listener, Document.this);
}
/**
* Remove a listener previously added through this facet. This will remove the listener from
* all combinations of timing and action for this document, even if added via multiple invocations
* of the <code>add</code> methods. However, it will not remove the listener from combinations
* added through other facets.
*
* @param listener the listener to remove
*/
public void remove(Document.Listener listener) {
// don't check for staleness here, might still want to remove listeners after doc is gone
ListenerManager.INSTANCE.remove(path(), ListenerManager.Depth.ZERO, listener);
}
}
/**
* The metadata facet for this document. Allows access to and manipulation of various aspects
* of the document's metadata, including its permissions and various timestamps.
* NOTE: The interface is fairly bare-bones right now, until I figure out the use cases and flesh
* it out a bit.
*/
public static class MetadataFacet extends NamedResource.MetadataFacet {
private final DocumentMetadata docMetadata;
private MetadataFacet(Permission permissions, DocumentMetadata docMetadata) {
super(permissions);
this.docMetadata = docMetadata;
}
@Override public Date creationDate() {return new Date(docMetadata.getCreated());}
/**
* Return the time at which this document was last modified.
*
* @return the date of the last modification
*/
public Date lastModificationDate() {return new Date(docMetadata.getLastModified());}
/**
* Return the recorded MIME type of this document.
*
* @return this document's MIME type
*/
public String mimeType() {return docMetadata.getMimeType();}
/**
* Set the MIME type of this document.
*
* @param mimeType this document's desired MIME type
*/
public void setMimeType(String mimeType) {docMetadata.setMimeType(mimeType);}
}
protected DocumentImpl doc;
protected StaleMarker staleMarker;
private ListenersFacet listeners;
private MetadataFacet metadata;
Document(DocumentImpl dimpl, NamespaceMap namespaceBindings, Database db) {
super(namespaceBindings, db);
changeDoc(dimpl);
}
private void changeDoc(DocumentImpl dimpl) {
if (dimpl == null) throw new NullPointerException("no such document");
assert getClass() == (dimpl instanceof BinaryDocument ? Document.class : XMLDocument.class);
this.doc = dimpl;
String path = dimpl.getURI().getCollectionPath();
staleMarker = new StaleMarker();
staleMarker.track(path.substring(0, path.lastIndexOf('/'))); // folder
staleMarker.track(path); // document
}
static Document newInstance(DocumentImpl dimpl, Resource origin) {
return newInstance(dimpl, origin.namespaceBindings().extend(), origin.database());
}
static Document newInstance(DocumentImpl dimpl, NamespaceMap namespaceBindings, Database db) {
return dimpl instanceof BinaryDocument ? new Document(dimpl, namespaceBindings, db) : new XMLDocument(dimpl, namespaceBindings, db);
}
@Override Sequence convertToSequence() {
// TODO: figure out if binary documents can be converted after all
throw new UnsupportedOperationException("binary resources are not convertible");
}
/**
* Return the listeners facet for this document, used for adding and removing document listeners.
*
* @return the listeners facet for this document
*/
public ListenersFacet listeners() {
if (listeners == null) listeners = new ListenersFacet();
return listeners;
}
@Override public MetadataFacet metadata() {
if (metadata == null) metadata = new MetadataFacet(doc.getPermissions(), doc.getMetadata());
return metadata;
}
/**
* Cast this document to an {@link XMLDocument}, if possible.
*
* @return this document cast as an XML document
* @throws DatabaseException if this document is not an XML document
*/
public XMLDocument xml() {
throw new DatabaseException("document is not XML");
}
@Override public boolean equals(Object o) {
if (o instanceof Document) return doc.getDocId() == ((Document) o).doc.getDocId();
return false;
}
@Override public int hashCode() {
return doc.getDocId();
}
/**
* Return a string representation of the reference to this document. The representation will
* list the document's path, but will not include its contents.
*
* @return a string representation of this document
*/
@Override public String toString() {
return "document '" + path() + "'";
}
/**
* Return the local filename of this document. This name will never contain
* slashes ('/').
*
* @return the local filename of this document
*/
@Override public String name() {
return doc.getFileURI().getCollectionPath();
}
/**
* Return the full path of this document. This is the path of its parent folder plus its
* filename.
*
* @return the full path of this document
*/
@Override public String path() {
// TODO: is this check necessary?
// if (doc.getURI() == null) throw new DatabaseException("handle invalid, document may have been deleted");
return Database.normalizePath(doc.getURI().getCollectionPath());
}
/**
* Return the folder that contains this document.
*
* @return the folder that contains this document
*/
public Folder folder() {
staleMarker.check();
String path = path();
int i = path.lastIndexOf('/');
assert i != -1;
return new Folder(i == 0 ? "/" : path.substring(0, i), false, this);
}
/**
* Return the length of this document, in bytes. For binary documents, this is the actual
* size of the file; for XML documents, this is the approximate amount of space that the
* document occupies in the database, and is unrelated to its serialized length.
*
* @return the length of this document, in bytes
*/
public long length() {
return doc.getContentLength();
}
/**
* Delete this document from the database.
*/
@Override public void delete() {
staleMarker.check();
folder().removeDocument(doc);
}
/**
* Copy this document to another collection, potentially changing the copy's name in the process.
* @see Name
*
* @param destination the destination folder for the copy
* @param name the desired name for the copy
* @return the new copy of the document
*/
@Override public Document copy(Folder destination, Name name) {
return newInstance(moveOrCopy(destination, name, true), this);
}
/**
* Move this document to another collection, potentially changing its name in the process.
* This document will refer to the document in its new location after this method returns.
* You can easily use this method to move a document without changing its name
* (<code>doc.move(newFolder, Name.keepCreate())</code>) or to rename a document
* without changing its location (<code>doc.move(doc.folder(), Name.create(newName))</code>).
* @see Name
*
* @param destination the destination folder for the move
* @param name the desired name for the moved document
*/
@Override public void move(Folder destination, Name name) {
changeDoc(moveOrCopy(destination, name, false));
}
private DocumentImpl moveOrCopy(Folder destination, Name name, boolean copy) {
db.checkSame(destination);
staleMarker.check();
name.setOldName(name());
return destination.moveOrCopyDocument(doc, name, copy);
}
/**
* Return the contents of this document interpreted as a string. Binary documents are
* decoded using the default character encoding specified for the database.
*
* @see Database#setDefaultCharacterEncoding(String)
* @return the contents of this document
* @throws DatabaseException if the encoding is not supported or some other unexpected IOException occurs
*/
public String contentsAsString() {
DBBroker broker = db.acquireBroker();
try {
InputStream is = broker.getBinaryResource((BinaryDocument) doc);
byte [] data = new byte[(int)broker.getBinaryResourceSize((BinaryDocument) doc)];
is.read(data);
is.close();
return new String(data, db.defaultCharacterEncoding);
} catch (UnsupportedEncodingException e) {
throw new DatabaseException(e);
} catch (IOException e) {
throw new DatabaseException(e);
} finally {
db.releaseBroker(broker);
}
}
/**
* Export this document to the given file, overwriting it if it already exists.
*
* @param destination the file to export to
* @throws IOException if the export failed due to an I/O error
*/
public void export(File destination) throws IOException {
OutputStream stream = new BufferedOutputStream(new FileOutputStream(destination));
try {
write(stream);
} finally {
stream.close();
}
}
/**
* Copy the contents of the document to the given stream. XML documents will use
* the default character encoding set for the database.
* @see Database#setDefaultCharacterEncoding(String)
*
* @param stream the output stream to copy the document to
* @throws IOException in case of I/O problems;
* WARNING: I/O exceptions are currently logged and eaten by eXist, so they won't propagate to this layer!
*/
public void write(OutputStream stream) throws IOException {
DBBroker broker = db.acquireBroker();
try {
broker.readBinaryResource((BinaryDocument) doc, stream);
} finally {
db.releaseBroker(broker);
}
}
@Override QueryService createQueryService() {
return QueryService.NULL;
}
}