/*
* Copyright 1999-2004 The Apache Software Foundation.
*
* 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.
*
* CVS $Id: Collection.java,v 1.52 2004/03/20 13:43:23 vgritsenko Exp $
*/
package org.apache.xindice.core;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xindice.core.data.DocumentSet;
import org.apache.xindice.core.data.EmptyDocumentSet;
import org.apache.xindice.core.data.EmptyNodeSet;
import org.apache.xindice.core.data.Key;
import org.apache.xindice.core.data.NodeSet;
import org.apache.xindice.core.data.Record;
import org.apache.xindice.core.data.RecordSet;
import org.apache.xindice.core.data.Value;
import org.apache.xindice.core.filer.Filer;
import org.apache.xindice.core.indexer.IndexManager;
import org.apache.xindice.core.indexer.Indexer;
import org.apache.xindice.core.meta.MetaData;
import org.apache.xindice.core.meta.inline.InlineMetaMap;
import org.apache.xindice.core.meta.inline.InlineMetaService;
import org.apache.xindice.core.meta.inline.ResourceTypeReader;
import org.apache.xindice.core.query.QueryEngine;
import org.apache.xindice.util.Configurable;
import org.apache.xindice.util.Configuration;
import org.apache.xindice.util.Named;
import org.apache.xindice.util.XindiceException;
import org.apache.xindice.xml.NamespaceMap;
import org.apache.xindice.xml.NodeSource;
import org.apache.xindice.xml.SymbolTable;
import org.apache.xindice.xml.TextWriter;
import org.apache.xindice.xml.XMLSerializable;
import org.apache.xindice.xml.dom.DBDocument;
import org.apache.xindice.xml.dom.DOMCompressor;
import org.apache.xindice.xml.dom.DOMParser;
import org.apache.xindice.xml.dom.DocumentImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.util.ArrayList;
/**
* Collection represents a collection of Documents maintains links to
* the Filer storage implementation, and the Indexes associated with
* the Collection.
*
* @version CVS $Revision: 1.52 $, $Date: 2004/03/20 13:43:23 $
*/
public class Collection extends CollectionManager implements Named, DBObject, Configurable {
private static final Log log = LogFactory.getLog(Collection.class);
private static final String CACHE = "cache";
private static final String CLASS = "class";
private static final String CLASSNAME = "xindice-class";
private static final String COMPRESSED = "compressed";
private static final String FILER = "filer";
private static final String INDEXES = "indexes";
private static final String INLINE_METADATA = "inline-metadata";
private static final String NAME = "name";
private static final String SYMBOLS = "symbols";
private static final DocumentSet EMPTY_DOCUMENTSET = new EmptyDocumentSet();
private static final NodeSet EMPTY_NODESET = new EmptyNodeSet();
private static final String[] EMPTY_STRING_ARRAY = {};
private static int host_id;
static {
try {
InetAddress a = InetAddress.getLocalHost();
byte[] b = a.getAddress();
host_id = 0;
host_id += b[0];
host_id += (b[1] << 8);
host_id += (b[2] << 16);
host_id += (b[3] << 24);
host_id = Math.abs(host_id);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
/**
* ColContainer
*/
private class ColContainer implements Container {
private Document document;
private Key key;
public ColContainer(Key key, Document document) {
this.key = key;
this.document = document;
}
public void commit() throws DBException {
putDocument(this.key, this.document /*, false */);
}
public void commit(Document doc) throws DBException {
this.document = doc;
commit();
}
public String getCanonicalName() throws DBException {
return Collection.this.getCanonicalDocumentName(key);
}
public Collection getCollection() {
return Collection.this;
}
public Document getDocument() {
return this.document;
}
public Key getKey() {
return this.key;
}
public void remove() throws DBException {
Collection.this.remove(key);
}
public Document rollback() throws DBException {
this.document = Collection.this.getDocument(key);
return this.document;
}
}
/**
* ColDocumentSet
*/
private class ColDocumentSet implements DocumentSet {
private RecordSet set;
public ColDocumentSet(RecordSet set) {
this.set = set;
}
public Container getNextContainer() throws DBException {
if (set.hasMoreRecords()) {
Record rec = set.getNextRecord();
Key key = rec.getKey();
Value val = rec.getValue();
if (val.getLength() > 0) {
try {
if (compressed) {
Document doc = new DocumentImpl(val.getData(), symbols, new NodeSource(Collection.this, key));
return new ColContainer(key, doc);
} else {
return new ColContainer(key, DOMParser.toDocument(val));
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
}
return null;
}
public Document getNextDocument() throws DBException {
Container c = getNextContainer();
if (c != null) {
return c.getDocument();
} else {
return null;
}
}
public boolean hasMoreDocuments() throws DBException {
return set.hasMoreRecords();
}
}
private String canonicalName;
// Object ID Stuff
private int collectionId;
private File collectionRoot;
private boolean compressed;
private long documentId = System.currentTimeMillis();
private DocumentCache documentCache;
private Filer filer;
private IndexManager indexManager;
private InlineMetaService inlineMetaService;
private boolean internalSymbols;
private String name;
private final Object oidMutex = new Object();
private String oidTemplate;
private Collection parent;
private SymbolTable symbols;
protected Collection() {
}
/**
* @param parentCollection
*/
public Collection(Collection parentCollection) {
this.parent = parentCollection;
}
private void checkFiler(int faultCode) throws DBException {
if (filer == null) {
throw new DBException(faultCode,
"Collection '" + name + "' cannot store resources (no filer)");
}
}
/**
* @see org.apache.xindice.core.DBObject#close()
*/
public boolean close() throws DBException {
if (filer != null) {
indexManager.close();
filer.close();
}
super.close();
return true;
}
/**
* @see org.apache.xindice.core.DBObject#create()
*/
public boolean create() throws DBException {
// update the meta information if necessary
updateCollectionMeta();
DBObserver.getInstance().createCollection(this);
return true;
}
/**
* @see org.apache.xindice.core.CollectionManager#createCollection(java.lang.String, org.apache.xindice.util.Configuration)
*/
public final Collection createCollection(String path, Configuration config) throws DBException {
Collection col = super.createCollection(path, config);
getDatabase().flushConfig();
return col;
}
/**
* createIndexer creates a new Indexer object and any associated
* system resources that the Indexer will need.
*
* @param config The Indexer's configuration
* @return The newly created Indexer
*/
public final Indexer createIndexer(Configuration config) throws DBException {
checkFiler(FaultCodes.COL_NO_INDEXMANAGER);
Indexer idx = indexManager.create(config);
getDatabase().flushConfig();
return idx;
}
/**
* createNewKey allocates a new key to be used as a key in the
* collection. Passed in <code>key</code> parameter string value
* used for the key. If passed key parameter is null, new OID is generated.
*
* @param key The Key hint, can be null
* @return The newly generated Key
*/
protected final Key createNewKey(Object key) {
if (key == null) {
return createNewOID();
} else if (key instanceof Key) {
return (Key) key;
} else {
return new Key(key.toString());
}
}
/**
* createNewOID allocates a new Object ID to be used as a Key in the
* Collection.
*
* @return The newly generated key
*/
public final Key createNewOID() {
long ct = System.currentTimeMillis();
synchronized (oidMutex) {
if (ct <= documentId) {
ct = documentId + 1;
}
documentId = ct;
}
StringBuffer sb = new StringBuffer(oidTemplate);
String document = Long.toString(documentId, 16);
sb.insert(32 - document.length(), document);
sb.setLength(32);
return new Key(sb.toString());
}
private String debugHeader() {
return "["
+ Thread.currentThread().getName()
+ "] '"
+ (parent != null ? parent.getCanonicalName() : "")
+ "/"
+ name
+ "' ";
}
/**
* @see org.apache.xindice.core.DBObject#drop()
*/
public boolean drop() throws DBException {
if (this == getDatabase()) {
throw new DBException(FaultCodes.DBE_CANNOT_DROP,
"You cannot drop the database");
}
DBObserver.getInstance().dropCollection(this);
// Drop the meta if necessary
if (isMetaEnabled()) {
getMetaSystemCollection().dropCollectionMeta(this);
}
// Drop Child Collections
String[] cols = listCollections();
for (int i = 0; i < cols.length; i++) {
dropCollection(getCollection(cols[i]));
}
if (filer != null) {
// Drop Indexers and Filer
indexManager.drop();
filer.drop();
}
getCollectionRoot().delete();
// Drop symbols
if (!internalSymbols) {
getSystemCollection().dropSymbols(this);
}
getDatabase().flushConfig();
return true;
}
/**
* @see org.apache.xindice.core.CollectionManager#dropCollection(org.apache.xindice.core.Collection)
*/
public final boolean dropCollection(Collection collection) throws DBException {
boolean success = super.dropCollection(collection);
getDatabase().flushConfig();
return success;
}
/**
* dropIndexer physically removes the specified Indexer and any
* associated system resources that the Indexer uses.
*
* @param index The Indexer to drop
* @return Whether or not the Indexer was dropped
*/
public final boolean dropIndexer(Indexer index) throws DBException {
checkFiler(FaultCodes.COL_NO_INDEXMANAGER);
if (index == null) {
throw new DBException(FaultCodes.IDX_INDEX_NOT_FOUND,
"Index value is null");
}
boolean success = indexManager.drop(index.getName());
getDatabase().flushConfig();
return success;
}
/**
* @see org.apache.xindice.core.DBObject#exists()
*/
public boolean exists() throws DBException {
return true;
}
/**
* @throws DBException
*/
public final void flushSymbolTable() throws DBException {
if (symbols.isDirty() && !internalSymbols) {
getSystemCollection().saveSymbols(this, symbols);
}
}
/**
* Retrieve a binary database entry by key.
* This low-level method will not update non-inline metadata.
*
* @param key identifying the desired database entry
* @return byte[] containing the binary database entry
* @throws DBException if inline-metadata is not enabled
* (binary resource cannot be stored in a collection
* which does not have inline-metadata enabled),
* in case of backing store error, and in case of
* header corruption
*/
public final byte[] getBinary(Object key) throws DBException {
if (log.isTraceEnabled()) {
log.trace(debugHeader() + "Get binary: " + key);
}
if (inlineMetaService == null) {
throw new DBException(FaultCodes.COL_DOCUMENT_NOT_FOUND,
"Collection '" + getCanonicalName() +
"' has no binary resources (inline metadata is not enabled)");
}
Object entry = getEntry(key);
if (entry == null) {
return null;
}
if (!(entry instanceof byte[])) {
throw new DBException(FaultCodes.COL_INVALID_RESULT,
"Resource '" + key + "' in collection '" +
getCanonicalName() + "' is not a binary resource");
}
return (byte[]) entry;
}
/**
* getCanonicalDocumentName returns the canonical name for the specified
* Key in relation to this Collection.
* <br>
* ex: /local/test/ocs/ytd
*
* @param key The Key
* @return The canonical name
*/
public final String getCanonicalDocumentName(Key key) {
return getCanonicalDocumentName(key.toString());
}
/**
* From the document key and this collection canonical name,
* composes canonical document name.
*
* @param key document key
* @return The canonical document name
*/
public final String getCanonicalDocumentName(String key) {
StringBuffer sb = new StringBuffer();
sb.append(canonicalName);
sb.append('/');
sb.append(key);
return sb.toString();
}
/**
* getCanonicalName returns the canonical name for this Object.
* <br>
* ex: /local/test/ocs
*
* @return The canonical name
*/
public final String getCanonicalName() {
return canonicalName;
}
/**
* Return the MetaData for this collection.
*
* If metadata is not enabled in the configuration, the MetaData object
* returned will be null.
*
* @return MetaData this collection's metadata.
*/
public MetaData getCollectionMeta() throws DBException {
if (!isMetaEnabled()) {
if (log.isWarnEnabled()) {
log.warn("Meta information requested but not enabled in config!");
}
return null;
}
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData meta = metacol.getCollectionMeta(this);
if (null == meta) {
long now = System.currentTimeMillis();
meta = new MetaData(MetaData.COLLECTION, getCanonicalName(), now, now);
metacol.setCollectionMeta(this, meta);
}
return meta;
}
/**
* @return The collection root
*/
public final File getCollectionRoot() {
return collectionRoot;
}
/**
* getContainer retrieves a Container from the Collection. The Container
* encapsulates all information needed in dealing with a Document outside
* of the context of a Collection (ex: DocumentContext).
*
* @param docKey The Document Key
* @return The Container
*/
public final Container getContainer(Object docKey) throws DBException {
Key key = createNewKey(docKey);
Document doc = getDocument(key);
return doc != null ? new ColContainer(key, doc) : null;
}
/**
* getDatabase returns the Database owner for this Collection.
*
* @return The Database
*/
public Database getDatabase() {
return parent.getDatabase();
}
/**
* getDocument retrieves a Document by Key.
*
* @param key The Document Key
* @return The Document
*/
public final Document getDocument(Object key) throws DBException {
if (log.isDebugEnabled()) {
log.debug(debugHeader() + "Get document: " + key);
}
Object entry = getEntry(key);
if (entry == null) {
return null;
}
if (!(entry instanceof Document)) {
throw new DBException(FaultCodes.COL_INVALID_RESULT,
"Resource '" + key + "' in collection '" +
getCanonicalName() + "' is not a document");
}
return (Document) entry;
}
/**
* getDocumentCount returns the count of Documents being maintained
* by this Collection.
*
* @return The Document count
*/
public final long getDocumentCount() throws DBException {
// a collection in which you are unable to file documents will have no filer
// (for example the root collection). Rather than throwing an exception return
// a constant result (nothing)
return null == filer ? 0 : filer.getRecordCount();
}
/**
* Return the MetaData object for a document within this collection.
*
* If metadata is not enabled, the MetaData object returned will be null.
* @param id the document whose metadata you want
*/
public MetaData getDocumentMeta(String id) throws DBException {
if (!isMetaEnabled()) {
if (log.isWarnEnabled()) {
log.warn("Meta information requested but not enabled in config!");
}
return null;
}
if (null == getEntry(id)) {
throw new DBException(FaultCodes.COL_DOCUMENT_NOT_FOUND,
"Resource '" + id + "' does not exist in '" + getCanonicalName() + "'");
}
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData meta = metacol.getDocumentMeta(this, id);
/*
TimeRecord rec = null;
if( null == meta || !meta.hasContext() )
rec = getDatabase().getTime(path);
long created = (null != rec) ? rec.getCreatedTime() : System.currentTimeMillis();
long modified = (null != rec) ? rec.getModifiedTime() : System.currentTimeMillis();
*/
// this is wrong.. but it should work for now...
long now = System.currentTimeMillis();
if (null == meta) {
meta = new MetaData(MetaData.DOCUMENT, getCanonicalDocumentName(id), now, now);
metacol.setDocumentMeta(this, id, meta);
} else if (!meta.hasContext()) {
meta.setContext(now, now);
}
return meta;
}
/**
* getDocumentSet returns the set of Documents being maintained
* by this Collection.
*
* @return The DocumentSet
*/
public final DocumentSet getDocumentSet() throws DBException {
// a collection in which you are unable to file documents will have no filer
// (for example the root collection). Rather than throwing an exception return
// a constant result (nothing)
return null == filer ? EMPTY_DOCUMENTSET : new ColDocumentSet(filer.getRecordSet());
}
/**
* Retrieve a database entry by key.
*
* If no matching entry is found, null is returned; if
* an XML entry is found, a Document is returned; if a
* binary entryis found, byte[] is returned.
*
* This low-level method will not update non-inline metadata.
*
* @param docKey identifying the desired database entry
* @return Object containing the database entry, or null if no
* matching entry is found
* @throws DBException in case of backing store error,
* and in case of header corruption
*/
public final Object getEntry(Object docKey) throws DBException {
// I would prefer to throw an exception (NPE) in this case,
// but we have a test indicating a null return...
if (docKey == null) {
return null;
}
String localDebugHeader = null;
if (log.isTraceEnabled()) {
localDebugHeader = debugHeader() + "getEntry: docKey=<" + docKey + ">: ";
log.trace(localDebugHeader);
}
checkFiler(FaultCodes.COL_NO_FILER);
Key key = createNewKey(docKey);
/*
* If the key has a corresponding value in the cache, return it
* and save a disk access.
*
* At some point the current document-centric cache implementation
* needs to be converted to an entry cache which can hold both
* Document and byte[].
*/
if (documentCache != null) {
Document document = documentCache.getDocument(this, key);
if (document != null) {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Returning cached: " + document);
}
return document;
}
}
Record record = filer.readRecord(key);
if (record == null) {
return null;
}
Value value;
InlineMetaMap metaMap = null;
if (inlineMetaService == null) {
value = record.getValue();
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Type is not available, Length=" + value.getLength());
}
} else {
InlineMetaService.DatabaseEntry databaseEntry = inlineMetaService.readDatabaseEntry(record.getValue());
metaMap = databaseEntry.map;
value = databaseEntry.value;
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Type=" + metaMap.get("type") + ", Length=" + value.getLength());
}
}
if (inlineMetaService == null || metaMap.get("type").equals(ResourceTypeReader.XML)) {
Document document;
if (compressed) {
document = new DocumentImpl(value.getData(), symbols, new NodeSource(this, key));
flushSymbolTable();
if (log.isTraceEnabled()) {
log.trace(localDebugHeader +
"Compressed XML document=<" + TextWriter.toString(document) + ">");
}
if (documentCache != null) {
documentCache.putDocument(this, key, value.getData());
}
} else {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Pre parseDocument(): value=<" + value + ">");
}
document = parseDocument(key, value.toString());
}
DBObserver.getInstance().loadDocument(this, record, document);
return document;
} else {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Binary document");
}
return value.getData();
}
}
/**
* getFiler returns the low-level Filer instances underlying the
* Collection instance.
*
* @return The requested Filer
*/
public final Filer getFiler() {
return filer;
}
/**
* getIndexer retrieves an Indexer by name.
*
* @param name The Indexer name
* @return The Indexer (or null)
*/
public final Indexer getIndexer(String name) throws DBException {
checkFiler(FaultCodes.COL_NO_INDEXMANAGER);
return indexManager.get(name);
}
/**
* return the IndexManager being used by this Collection.
*
* @return The IndexManager
*/
public final IndexManager getIndexManager() throws DBException {
checkFiler(FaultCodes.COL_NO_INDEXMANAGER);
return indexManager;
}
/**
* Return the MetaSystemCollection for the database containing this
* collection.
*
* @return MetaSystemCollection
*/
private MetaSystemCollection getMetaSystemCollection() {
return getDatabase().getMetaSystemCollection();
}
public final String getName() {
return name;
}
/**
* getObject instantiates and returns an XMLSerializable object based on the
* provided Key. Xindice takes care of instantiating the correct class, but
* only if a class was registered with the Document in the first place.
*
* @param key The Document Key
* @return an Castable XMLSerializable Instance
*/
public final XMLSerializable getObject(Object key) throws DBException {
if (log.isDebugEnabled()) {
log.debug(debugHeader() + "Get object: " + key);
}
String className = null;
Document doc = getDocument(key);
if (doc != null) {
NodeList childNodes = doc.getChildNodes();
int size = childNodes.getLength();
for (int i = 0; i < size; i++) {
Node n = childNodes.item(i);
if (n.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE && n.getNodeName().equals(CLASSNAME)) {
className = n.getNodeValue().trim();
break;
}
}
if (className != null) {
try {
XMLSerializable obj = (XMLSerializable) Class.forName(className).newInstance();
obj.streamFromXML(doc.getDocumentElement());
return obj;
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("ignored exception", e);
}
}
}
}
return null;
}
/**
* getParentCollection returns the parent Collection of this
* Collection.
*
* @return The parent Collection (or null)
*/
public final Collection getParentCollection() throws DBException {
return parent;
}
/**
* getQueryEngine returns the Database's Query Engine
*
* @return The Query Engine
*/
public QueryEngine getQueryEngine() throws DBException {
return getDatabase().getQueryEngine();
}
/**
* getSymbols returns the SymbolTable in use by this
* Collection.
*
* @return The Symbol Table
*/
public final SymbolTable getSymbols() throws DBException {
return symbols;
}
/**
* getSystemCollection returns the System Collection.
*
* @return The System Collection
*/
public SystemCollection getSystemCollection() throws DBException {
return getDatabase().getSystemCollection();
}
/**
* Insert a binary object into a Xindice Collection. A unique key
* is automatically generated. by which the binary object can be
* retrieved in the future. Note: because the key is automatically
* unique, this insert method will never cause a collision with an
* object already in the database.
*
* @param bytes The bytes making up the binary object to insert
* @return Key automatically generated for the binary object
* @throws DBException if inline-metadata is not enabled, or an
* error occurs while saving.
*/
public Key insertBinary(byte[] bytes) throws DBException {
return insertBinary(null, bytes);
}
/**
* insertBinary inserts a new binary object into a Xindice Collection.
*
* @param docKey The document Key
* @param bytes The document to insert
* @throws DBException if inline-metadata is not enabled, the key is
* already in the database, or an error occurs while saving.
*/
public Key insertBinary(Object docKey, byte[] bytes) throws DBException {
if (inlineMetaService == null) {
throw new DBException(FaultCodes.COL_CANNOT_STORE,
"Cannot insert a binary resource in '" + getCanonicalName() +
"' (inline-metadata is not enabled)");
}
Key key = createNewKey(docKey);
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Insert binary: " + key);
}
putBinary(key, bytes, true);
// update the meta information if necessary
updateCollectionMeta();
return key;
}
/**
* insertDocument inserts a new Document into a Xindice Collection.
*
* @param document The Document
* @return The new Object Identifier
*/
public final Key insertDocument(Document document) throws DBException {
return insertDocument(null, document);
}
/**
* insertDocument inserts a new Document into a Xindice Collection.
*
* @param docKey The document Key
* @param document The document to insert
*/
public final Key insertDocument(Object docKey, Document document) throws DBException {
Key key = createNewKey(docKey);
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Insert document: " + key);
}
putDocument(key, document /*, true */);
// update the meta information if necessary
updateCollectionMeta();
return key;
}
/**
* insertObject inserts an XMLSerializable object into the Collection and
* returns a newly generated Key. Xindice takes care of associating the
* implementation class with the XMLSerializable object.
*
* @param obj The Object to insert
* @return The newly generated Key
*/
public final Key insertObject(XMLSerializable obj) throws DBException {
return insertObject(null, obj);
}
/**
* insertObject inserts an XMLSerializable object into the Collection based
* on the specified Key. Xindice takes care of associating the
* implementation class with the XMLSerializable object.
*
* @param objKey The Key to use
* @param obj The Object to insert
*/
public final Key insertObject(String objKey, XMLSerializable obj) throws DBException {
Key key = createNewKey(objKey);
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Insert object: " + key);
}
putObject(key, obj /*, true */);
// update the meta information if necessary
updateCollectionMeta();
return key;
}
/**
* Returns whether or not meta data is enabled.
* @return boolean whether or not meta data is enabled.
*/
public boolean isMetaEnabled() {
return getDatabase().isMetaEnabled();
}
public boolean isOpened() throws DBException {
return true;
}
/**
* listDocuments returns a list of all document keys stored by this
* collection.
*
* @return the list of document keys
*/
public final String[] listDocuments() throws DBException {
// a collection in which you are unable to file documents will have no filer
// (for example the root collection). Rather than throwing an exception return
// a constant result (nothing)
if (null == filer) {
return EMPTY_STRING_ARRAY;
} else {
// TODO: ArrayList length is limited to the int, while filer record count is long
// give a hint to the size of the record set, saves on arraylist array copies.
ArrayList temp = new ArrayList((int) filer.getRecordCount());
RecordSet set = filer.getRecordSet();
while (set.hasMoreRecords()) {
Key key = set.getNextKey();
temp.add(key.toString());
}
return (String[]) temp.toArray(new String[0]);
}
}
/**
* listIndexers returns a list of the currently registered Indexers
* as an array of String.
*
* @return The Indexer list
*/
public final String[] listIndexers() throws DBException {
checkFiler(FaultCodes.COL_NO_INDEXMANAGER);
return indexManager.list();
}
public final boolean open() throws DBException {
return true;
}
/**
* Turns an XML string into a parsed document and caches it.
*
* @param key The key to use when caching
* @param xml The string to parse
* @return A parsed DOM document or null if failure
*/
private Document parseDocument(Key key, String xml) throws DBException {
Document doc = null;
try {
doc = DOMParser.toDocument(xml);
// Have to move it to Xindice DOM for XMLObject AutoLinking
byte[] b = DOMCompressor.Compress(doc, symbols);
doc = new DocumentImpl(b, symbols, new NodeSource(this, key));
if (documentCache != null) {
documentCache.putDocument(this, key, b);
}
} catch (Exception e) {
throw new DBException(FaultCodes.COL_DOCUMENT_MALFORMED,
"Unable to parse document '" + key + "' in '" + getCanonicalName() + "'", e);
}
return doc;
}
/*
* Lowest-level method for saving a binary entry into the database. At this moment,
* presence of inline metadata is known.
* It now does update non-inline metadata if the user has configured it.
*/
private void putBinary(Key key, byte[] bytes, boolean create) throws DBException {
if (!create) {
byte[] storedBytes = getBinary(key);
if (storedBytes == null) {
// TODO: Do we need a COL_KEY_ALREADY_PRESENT fault so that the caller can interpret this exception?
throw new DBException(FaultCodes.COL_CANNOT_STORE,
"Error storing binary resource '" + key + "' in '" + getCanonicalName() +
"': the 'create' flag is false and the key is already in database");
}
}
InlineMetaMap map = inlineMetaService.getEmptyMap();
map.put("type", ResourceTypeReader.BINARY);
Value value = inlineMetaService.createValue(map, bytes, 0, bytes.length);
filer.writeRecord(key, value);
// update the meta for this document
updateDocumentMeta(key.toString());
}
/**
* This is the lowest-level method for storing a record into the backing store.
* It now does update non-inline metadata if the user has configured it.
*/
private void putDocument(Key key, Document document) throws DBException {
final String localDebugHeader = debugHeader() + "putDocument: docKey=<" + key + ">: ";
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "document=<" + TextWriter.toString(document) + ">");
}
checkFiler(FaultCodes.COL_NO_FILER);
if (document instanceof DBDocument) {
// FIXME: This is a shitty shitty hack... Kill immediately
DBDocument dbDoc = (DBDocument) document;
if (dbDoc.getSource() == null) {
dbDoc.setSource(new NodeSource(this, key));
}
}
/*
* The possibilities are restricted because only XML
* is handled by this method. There are only a few
* pieces of information that need to be constructed:
* 1) the xindice DOM document is needed for all XML objects, as
* it is handed to the IndexManager and the DBObserver.
* 2) the packed document, if this is a compressed XML object,
* is needed for the cache and the BTree (via the Value object).
* 3) the string-converted-to-utf-8 bytes, if this is a non-compressed
* XML object, is needed for the BTree (via the Value object).
* 4) A Value object, with a header if headers are enabled, and
* otherwise without headers, for the BTree.
*/
byte[] documentBytes = null;
if (compressed) {
try {
documentBytes = DOMCompressor.Compress(document, symbols);
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "length=" + documentBytes.length);
}
// Why must it be re-created?
document = new DocumentImpl(documentBytes, symbols, new NodeSource(this, key));
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "packedDocument: length=" + documentBytes.length +
" document=<" + TextWriter.toString(document) + ">");
}
} catch (Exception e) {
throw new DBException(FaultCodes.COL_CANNOT_STORE,
localDebugHeader + "Error compressing Document '" + key + "'", e);
}
} else {
try {
documentBytes = TextWriter.toString(document).getBytes("utf-8");
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "utf8Document: length=" + documentBytes.length +
" document=<" + new String(documentBytes, "utf-8") + ">");
}
} catch (UnsupportedEncodingException e) {
// Should never happen
throw new DBException(FaultCodes.GEN_FATAL_ERROR,
"utf-8 encoding not supported", e);
}
}
// TODO: Move into if(compressed) ?
flushSymbolTable();
// Temporary until insert and update are separate
Document oldDoc = getDocument(key);
if (oldDoc != null) {
indexManager.removeDocument(key, oldDoc);
}
indexManager.addDocument(key, document);
/*
* Construct the Value object that is stored in the BTree.
*/
Value value;
if (inlineMetaService == null) {
value = new Value(documentBytes);
} else {
InlineMetaMap map = inlineMetaService.getEmptyMap();
map.put("type", ResourceTypeReader.XML);
value = inlineMetaService.createValue(map, documentBytes, 0, documentBytes.length);
}
filer.writeRecord(key, value);
// Cache Stuff
if (documentCache != null) {
if (compressed) {
documentCache.putDocument(this, key, documentBytes);
} else {
documentCache.putDocument(this, key, document);
}
}
// update the meta for this document
updateDocumentMeta(key.toString());
DBObserver.getInstance().putDocument(this, key, document, oldDoc == null);
}
private void putObject(Key key, XMLSerializable obj) throws DBException {
if (log.isTraceEnabled()) {
log.trace(debugHeader() + "putObject: key=<" + key + "> class=<" + obj.getClass().getName() + ">");
}
Document doc = new DocumentImpl();
ProcessingInstruction pi = doc.createProcessingInstruction(CLASSNAME, obj.getClass().getName());
doc.appendChild(pi);
Element elem = obj.streamToXML(doc);
doc.appendChild(elem);
putDocument(key, doc /*, create */);
}
/**
* queryCollection performs a query against the current collection
* using the specified style and query String.
*
* @param style The query style to use (ex: XPath)
* @param query The query to execute
* @param nsMap The namespace Map (if any)
* @return The resulting NodeSet
*/
public final NodeSet queryCollection(String style, String query, NamespaceMap nsMap) throws DBException {
if (log.isDebugEnabled()) {
log.debug(debugHeader() + "Query collection, query " + query);
}
// A collection in which you are unable to file documents will have no filer
// (for example the root collection). Rather than throwing an exception return
// a constant result (nothing)
return null == filer ? EMPTY_NODESET : getQueryEngine().query(this, style, query, nsMap, null);
}
/**
* queryDocument performs a query against a single Document using
* the specified style, query string, and Document ID.
*
* @param style The query style to use (ex: XPath)
* @param query The query to execute
* @param nsMap The namespace Map (if any)
* @param key The Document to query
* @return The resulting NodeSet
*/
public final NodeSet queryDocument(String style, String query, NamespaceMap nsMap, Object key) throws DBException {
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Query document " + key + ", query: " + query);
}
checkFiler(FaultCodes.QRY_STYLE_NOT_FOUND);
Key[] k = null;
if (key instanceof Key[]) {
k = (Key[]) key;
} else {
k = new Key[]{createNewKey(key)};
}
return getQueryEngine().query(this, style, query, nsMap, k);
}
/**
* remove removes an object from the Collection based on its Key,
* regardless of it's type.
*
* @param key The Object's Key
*/
public final void remove(Object key) throws DBException {
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Remove " + key);
}
checkFiler(FaultCodes.COL_NO_FILER);
Key objKey = createNewKey(key);
Object oldDoc = getEntry(objKey);
if (oldDoc != null && oldDoc instanceof Document) {
indexManager.removeDocument(objKey, (Document)oldDoc);
}
if (documentCache != null) {
documentCache.removeDocument(this, objKey);
}
if (!filer.deleteRecord(objKey)) {
throw new DBException(FaultCodes.COL_DOCUMENT_NOT_FOUND,
"Resource '" + objKey + "' does not exist in '" + getCanonicalName() + "'");
}
// update the meta for this collection if necessary
updateCollectionMeta();
// remove the document meta
if (isMetaEnabled()) {
getMetaSystemCollection().dropDocumentMeta(this, objKey.toString());
}
DBObserver.getInstance().dropDocument(this, objKey);
}
protected final void setCanonicalName(String canonicalName) {
this.canonicalName = canonicalName;
// Calculate The OID Template
collectionId = Math.abs(canonicalName.hashCode());
StringBuffer sb = new StringBuffer("00000000000000000000000000000000");
String host = Integer.toString(host_id, 16);
String collection = Integer.toString(collectionId, 16);
sb.insert(8 - host.length(), host);
sb.insert(16 - collection.length(), collection);
sb.setLength(32);
oidTemplate = sb.toString();
}
/**
* Reset the metadata object for this collection.
* @param meta the Metadata to use
*/
public void setCollectionMeta(MetaData meta) throws DBException {
if (!isMetaEnabled()) {
if (log.isWarnEnabled()) {
log.warn("Meta information requested but not enabled in config!");
}
return;
}
if (null != meta) {
if (meta.getType() != MetaData.COLLECTION) {
throw new DBException(FaultCodes.GEN_UNKNOWN,
"Mismatch type of meta data for collection " + getCanonicalName());
}
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData current = metacol.getCollectionMeta(this);
current.copyDataFrom(meta);
metacol.setCollectionMeta(this, current);
}
}
protected final void setCollectionRoot(File collectionRoot) {
this.collectionRoot = collectionRoot;
if (!collectionRoot.exists()) {
if (log.isTraceEnabled()) {
log.trace("Creating directories: " + collectionRoot);
}
collectionRoot.mkdirs();
}
}
public void setConfig(Configuration config) throws XindiceException {
name = config.getAttribute(NAME);
compressed = config.getBooleanAttribute(COMPRESSED, true);
/*
* If inline metadata is desired, get an InlineMetaService object.
*/
if (config.getBooleanAttribute(INLINE_METADATA, false)) {
inlineMetaService = new InlineMetaService();
}
/*
* Wait to set up the local debug header until everything needed
* by debugHeader() is complete!
*/
final String localDebugHeader = debugHeader() + "setConfig: ";
// Set parent
if (parent != null) {
setCanonicalName(parent.getCanonicalName() + '/' + name);
setCollectionRoot(new File(parent.getCollectionRoot(), name));
if (log.isDebugEnabled()) {
log.debug(localDebugHeader + "Root=<" + getCollectionRoot() + ">");
}
}
if (log.isDebugEnabled()) {
log.debug(localDebugHeader
+ (compressed ? "Compressed" : "NOT Compressed")
+ ", "
+ (inlineMetaService == null ? "Inline metadata DISABLED" : "Inline metadata ENABLED")
);
}
if (config.getBooleanAttribute(CACHE, true)) {
documentCache = getDatabase().getDocumentCache();
}
// If no Filer is defined, skip Symbols and Indexes
Configuration filerConfig = config.getChild(FILER);
if (filerConfig != null) {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "Have filer config...");
}
// Symbol Table Setup
Configuration symConfig = config.getChild(SYMBOLS);
internalSymbols = (symConfig != null);
if (internalSymbols) {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader
+ "Internal symbols=<" + TextWriter.toString(symConfig.getElement()) + ">");
}
try {
symbols = new SymbolTable(symConfig.getElement());
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn(localDebugHeader + "Error building symbol table from internal symbols", e);
}
}
} else {
if (log.isTraceEnabled()) {
log.trace(localDebugHeader + "No internal symbols...");
}
try {
symbols = getSystemCollection().loadSymbols(this);
if (log.isDebugEnabled()) {
log.debug(localDebugHeader + "Loaded symbols=<" +
TextWriter.toString(symbols.streamToXML(new DocumentImpl())) + ">");
}
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn(localDebugHeader + "Error building symbol table from system collection", e);
}
}
}
String className = filerConfig.getAttribute(CLASS);
if (log.isDebugEnabled()) {
log.debug(localDebugHeader + "Filer class=<" + className + ">");
}
try {
filer = (Filer) Class.forName(className).newInstance();
// filer.setCollection(this);
filer.setLocation(getCollectionRoot(), getName());
filer.setConfig(filerConfig);
if (!filer.exists()) {
filer.create();
}
filer.open();
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Filer '" + className + "' is not available", e);
}
}
// Index Manager
try {
indexManager = new IndexManager(this);
Configuration idxConfig = config.getChild(INDEXES, true);
indexManager.setConfig(idxConfig);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Failed to initialize indexer", e);
}
}
}
// Last thing to do is to init child collections
super.setConfig(config);
// observer
DBObserver.getInstance().setCollectionConfig(this, config);
}
/**
* setDocument overwrites/updates an existing Document in a
* Xindice Collection.
*
* @param docKey The Document Key
* @param document The Document
*/
public final void setDocument(Object docKey, Document document) throws DBException {
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Set document " + docKey);
}
putDocument(createNewKey(docKey), document /*, false */);
}
/**
* Set the metadata associated with a document within this collection.
*
* @param id the document name
* @param meta the metadata object to be used.
*/
public void setDocumentMeta(String id, MetaData meta) throws DBException {
if (!isMetaEnabled()) {
if (log.isWarnEnabled()) {
log.warn("Meta information requested but not enabled in config!");
}
return;
}
Object obj = getEntry(id);
if (null == obj) {
throw new DBException(FaultCodes.COL_DOCUMENT_NOT_FOUND,
"Resource '" + id + "' does not exist in '" + getCanonicalName() + "'");
}
if (null != meta) {
if (meta.getType() == MetaData.UNKNOWN || meta.getType() == MetaData.COLLECTION) {
throw new DBException(FaultCodes.GEN_UNKNOWN,
"Mismatch type of meta data for document " + getCanonicalDocumentName(id));
}
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Set document meta " + id);
}
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData current = metacol.getDocumentMeta(this, id);
current.copyDataFrom(meta);
metacol.setDocumentMeta(this, id, current);
}
}
/**
* @param string
*/
protected void setName(String string) {
name = string;
}
/**
* setObject sets an XMLSerializable object in the Collection based on the
* provided Key. Xindice takes care of associating the implementation class
* with the XMLSerializable object.
*
* @param key The Key to use
* @param obj The Object to set
*/
public final void setObject(Object key, XMLSerializable obj) throws DBException {
if (log.isInfoEnabled()) {
log.info(debugHeader() + "Set object " + key);
}
putObject(createNewKey(key), obj /*, false */ );
}
/**
* update the modified time of this collection when appropriate
*/
protected void updateCollectionMeta() {
// update the meta data if necessary
if (isMetaEnabled()) {
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData meta = null;
try {
meta = metacol.getCollectionMeta(this);
} catch (DBException e) {
// something strange has happened.. can't get the
// meta data for this collection
if (log.isWarnEnabled()) {
log.warn("Error fetching meta for collection '" + getCanonicalName() + "'", e);
}
return;
}
if (log.isTraceEnabled()) {
log.trace(debugHeader() + "Updating modified time for collection");
}
long now = System.currentTimeMillis();
if (null == meta) {
meta = new MetaData(MetaData.COLLECTION, getCanonicalName(), now, now);
} else if (!meta.hasContext()) {
// Newly created meta. Update its created and modified time.
meta.setContext(now, now);
} else {
// This collection already has a meta. So update its modified time.
meta.setContext(0, now);
}
try {
metacol.setCollectionMeta(this, meta);
} catch (DBException e) {
if (log.isWarnEnabled()) {
log.warn("Error setting meta for collection '" + getCanonicalName() + "'", e);
}
return;
}
}
}
/**
* update the modified time of this document when appropriate
*/
protected void updateDocumentMeta(String id) throws DBException {
// update the meta data if necessary
if (!isMetaEnabled()) {
return;
}
Object obj = getEntry(id);
if (null == obj) {
throw new DBException(FaultCodes.COL_DOCUMENT_NOT_FOUND,
"Resource '" + id + "' does not exist in '" + getCanonicalName() + "'");
}
MetaSystemCollection metacol = getMetaSystemCollection();
MetaData meta = metacol.getDocumentMeta(this, id);
String path = getCanonicalDocumentName(id);
/*
TimeRecord rec = null;
if( null == meta || !meta.hasContext() )
rec = getDatabase().getTime(path);
long created = (null != rec) ? rec.getCreatedTime() : System.currentTimeMillis();
long modified = (null != rec) ? rec.getModifiedTime() : System.currentTimeMillis();
*/
// this is wrong.. but it should work for now...
if (log.isTraceEnabled()) {
log.trace(debugHeader() + "Updating modified time for document '" + id + "'");
}
long now = System.currentTimeMillis();
if (null == meta) {
meta = new MetaData(MetaData.DOCUMENT, path, now, now);
} else if (!meta.hasContext()) {
meta.setContext(now, now);
} else {
meta.setContext(0, now);
}
metacol.setDocumentMeta(this, id, meta);
}
}