Package org.apache.xindice.core

Source Code of org.apache.xindice.core.Collection$ColDocumentSet

/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements.  See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*
* $Id: Collection.java 530223 2007-04-19 01:48:06Z vgritsenko $
*/

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.lang.ref.WeakReference;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Map;
import java.util.WeakHashMap;

/**
* Collection represents a collection of Documents maintains links to
* the Filer storage implementation, and the Indexes associated with
* the Collection.
*
* @version $Revision: 530223 $, $Date: 2007-04-18 21:48:06 -0400 (Wed, 18 Apr 2007) $
*/
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;
    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;

    // document keys identity map
    private final Map identityMap = new WeakHashMap();


    protected Collection() {
        documentId = System.currentTimeMillis();
    }

    /**
     * @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;
        }

        Key key = getIdentityKey(createNewKey(id));
        synchronized (key) {
            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);

            /*
            FIXME It is more efficient to store (and retrieve) created/modified timestamps
                  from the Record itself instead of storing them in the separate MetaData
                  object. Storing in the Record avoids writing two documents on each update
                  (Document itself and its MetaData).
                  Retrieval of the timestamps from Record can be implemented via TimeRecord.

            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 = getIdentityKey(createNewKey(docKey));
        synchronized (key) {

            /*
             * 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 {
                    String documentChars = value.toString();
                    if (log.isTraceEnabled()) {
                        log.trace(localDebugHeader + "Pre parseDocument(): value=<" + documentChars + ">");
                    }

                    // FIXME These should be no reason here to re-compress the document & flush symbols table?
                    document = parseDocument(key, documentChars);
                    flushSymbolTable();

                    if (documentCache != null) {
                        documentCache.putDocument(this, key, documentChars);
                    }
                }

                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);
        }

        Document doc = getDocument(key);
        if (doc != null) {
            String className = 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[temp.size()]);
        }
    }

    /**
     * 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 retrieved
     * from the uncompressed collection.
     *
     * @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 {
        try {
            Document doc = DOMParser.toDocument(xml);
            ((DBDocument) doc).setSource(new NodeSource(this, key));

            // Have to compress to update collection's SymbolTable,
            // which is used even for uncompressed collections
            DOMCompressor.compress(doc, symbols);

            return doc;
        } catch (Exception e) {
            throw new DBException(FaultCodes.COL_DOCUMENT_MALFORMED,
                                  "Unable to parse document '" + key + "' in '" + getCanonicalName() + "'", e);
        }
    }

    /*
     * 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 {
        synchronized (getIdentityKey(key)) {
            Object entry = getEntry(key);
            if (!create) {
                if (entry == 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");
                }
            }

            if (entry != null && entry instanceof Document) {
                // binary resources aren't stored in cache or indexes
                if (documentCache != null) {
                    documentCache.removeDocument(this, key);
                }
                indexManager.removeDocument(key, (Document) entry);
            }

            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;
        String documentChars = null;

        // FIXME: concurrent symbol table access.
        if (compressed) {
            // Create compressed document bytes to be stored in the filer
            documentBytes = DOMCompressor.compress(document, symbols);
            if (log.isTraceEnabled()) {
                log.trace(localDebugHeader + "length=" + documentBytes.length);
            }

            // Create xindice document with just compressed bytes.
            // Passed in document might not necessarily be xindice document,
            // but we should be passing only our documents to index manager.
            document = new DocumentImpl(documentBytes, symbols, new NodeSource(this, key));

            if (log.isTraceEnabled()) {
                log.trace(localDebugHeader + "packedDocument: length=" + documentBytes.length +
                          " document=<" + TextWriter.toString(document) + ">");
            }
        } else {
            // Create uncompressed document bytes to be stored in the filer
            documentChars = TextWriter.toString(document);
            try {
                documentBytes = documentChars.getBytes("utf-8");
            } catch (UnsupportedEncodingException e) {
                // Should never happen
                throw new DBException(FaultCodes.GEN_FATAL_ERROR,
                                      "utf-8 encoding not supported", e);
            }

            // Create xindice document from the string.
            // In addition to converting passed document to xindice document
            // instance, parseDocument() also updates the symbol table,
            // if necessary.
            document = parseDocument(key, documentChars);

            if (log.isTraceEnabled()) {
                log.trace(localDebugHeader + "utf8Document: length=" + documentBytes.length +
                          " document=<" + documentChars + ">");
            }
        }

        // Symbol table could have been updated above, flush it to the disk.
        flushSymbolTable();

        key = getIdentityKey(key);
        Object oldDoc;
        synchronized (key) {
            // Temporary until insert and update are separate
            oldDoc = getEntry(key);
            if (oldDoc != null && oldDoc instanceof Document) {
                indexManager.removeDocument(key, (Document) 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, documentChars);
                }
            }

            // 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;
        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);

        objKey = getIdentityKey(objKey);
        synchronized (objKey) {
            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.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, getDatabase().getTimer());
                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);
    }

    /**
     * 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;
        }

        Key key = getIdentityKey(createNewKey(id));
        synchronized (key) {
            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;
            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);
                }
            }
        }
    }

    /**
     * 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);
    }

    private Key getIdentityKey(Key key) {
        synchronized (identityMap) {
            Key id = null;
            WeakReference ref = (WeakReference) identityMap.get(key);
            if (ref != null) {
                id = (Key) ref.get();
            }
            if (id == null) {
                id = key;
                identityMap.put(id, new WeakReference(id));
            }

            return id;
        }
    }
}
TOP

Related Classes of org.apache.xindice.core.Collection$ColDocumentSet

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.