CDatabase.java :  » Database-DBMS » perst » org » garret » perst » continuous » Java Open Source

Java Open Source » Database DBMS » perst 
perst » org » garret » perst » continuous » CDatabase.java
package org.garret.perst.continuous;

import java.util.*;
import java.io.*;
import org.garret.perst.*;
import org.garret.perst.impl.ClassDescriptor;

import org.apache.lucene.index.Term;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.queryParser.*;
import org.apache.lucene.search.Hits;
import org.apache.lucene.analysis.standard.StandardAnalyzer;

/**
 * <p>
 * This class emulates relational database on top of Perst storage
 * It maintains class extends and associated indices. Is supports JSQL and full text search queries.
 * This class provides version control system, optimistic access control and full text search (using Lucene).
 * </p><p>
 * All transactions are isolated from each other and do not see changes made by other transactions 
 * (even committed transactions if commit happens after start of this transaction).
 * The database keeps all versions of the object hich are grouped in version history.
 * Version history is linear which means that no branches in version tree are possible. 
 * When transaction tries to update some object then working copy of the current version is created.
 * Only this copy is changed, leaving all other versions unchanged. This modification will be visible only
 * for the current transaction. Created or modified objects are linked in indices when transaction is committed.
 * It means that application will not be able to find in the index just inserted or updated object. It has 
 * first to commit the current transaction. All working copies created during transaction execution
 * are linked in he list stored in transaction context which is in turn associated with the current thread. 
 * It means two things:
 * <ol>
 * <li>one thread can participate in only one transaction and one transaction can be controlled only by one thread</li>
 * <li>size of transaction is limited by the amount of memory available for the application</li>
 * </ol>
 * When transaction is committed, then all its working copies are inspected. If the last version in version history is 
 * not equal to one from which working copy was created then conflict is detected and ConflictException is thrown.
 * And if insertion of working copy in index will cause unique constraint violation then NotUniqueException is thrown.
 * If no conflicts are detected, then transaction is committed and each working copy becomes new version in correspondent 
 * version history.
 * </p><p>
 * Isolation of transaction provided by CDatabase class is based on multiversioning and modification of working copies
 * instead of original objects. Working copy is produced from the current version using clone (shallow copy) method.
 * Clone will work correctly if components of the objects have primitive type (int, double,...) or are immutable (String). 
 * Using java.util.Date type is possible only if you do not try to update its components. 
 * Instead of normal Java references, you should use references to correspondent CVersionHistory (it allows to preserve 
 * reference consistency if new version of reference object is created).
 * CDatabase class also supports links (components with Link type) and arrays. 
 * Fields with <i>value type</i> (class implementing IValue interface) can be used only if them are 
 * immutable or cloneable (implement org.garret.perst.ICloneable interface). 
 * Using any other data type including Perst collection classes is not supported and can cause unpredictable behavior.
 */
public class CDatabase { 
    /** 
     * Open the database. This method initialize database if it not initialized yet.
     * @param storage opened storage. Storage should be either empty (non-initialized, either
     * previously initialized by the this method. It is not possible to open storage with 
     * root object other than RootObject  created by this method.
     * @param fullTextIndexPath path to the directory containing Lucene database. If null, then Lucene index will be stored 
     * inside the main Perst storage.
     */
    public void open(Storage storage, String fullTextIndexPath) 
    { 
        this.storage = storage;
        storage.setProperty("perst.concurrent.iterator", Boolean.TRUE);
        root = (RootObject)storage.getRoot();
        typeMap = new HashMap<Class,TableDescriptor>();
        if (root == null) { 
            root = new RootObject(storage);
            storage.setRoot(root);
        } else { 
            for (TableDescriptor desc : root.tables) { 
                typeMap.put(desc.type, desc);
            }
        }
        openFullTextIndex(root, fullTextIndexPath);
        storage.commit();
    }

    /**
     * Close the database. All uncommitted transaction will be aborted.
     * Close full text index and storage. 
     */
    public void close() 
    { 
        try { 
            if (indexWriter != null) { 
                indexWriter.close();
                indexWriter = null;
            }
            if (indexReader != null) { 
                indexReader.close();
                indexReader = null;
            }
            if (dir != null) { 
                dir.close();
                dir = null;
            }
        } catch (IOException x) { 
            throw new IOError(x);
        }
        storage.close();
        storage = null;

        root = null;
        typeMap = null;
        analyzer = null;
    }        

    /**
     * Start new transaction. All changes can be made only within transaction context. 
     * Transaction context is associated with thread. It means that one thread can run only one transaction and
     * one transaction can be handled only by one thread. 
     * Transaction is observing database state at the moment of the transaction start. Any changes done by
     * other transactions (committed and uncommitted) after this moment are not visible for the current transaction.
     * CDatabase uses optimistic access control. It means that transaction may be aborted if conflict 
     * is detected during transaction commit.
     */
    public void beginTransaction() 
    {
        beginTransaction(root.getLastTransactionId());
    }

    /**
     * Start transaction with the given identifier. 
     * Identifier of the transaction can be obtained using CVersion.getTransactionId() method.
     * Starting transaction with ID of previously committed transaction allows to obtain snapshot
     * of the database at the moment of this transaction execution. All search methods
     * using default version selector (referencing current version) and CVersionHistory.getCurrent()
     * method will select versions which were current at the moment of this transaction execution.
     * @param transId identifier of previously committed transaction
     * @exception TransactionAlreadyStartedException when thread attempts to start new transaction without committing or 
     * aborting previous one. Transaction should be explicitly finished using CDatabase.commit or CDatabase.rollback method.
     * Each thread should start its own transaction, it is not possible to share the same transaction by more than one threads.
     * It is possible to call beginTransaction several times without commit or rollback only if previous transaction was read-only 
     * - didn't change any object
     */
    public synchronized void beginTransaction(long transId) 
    {
        TransactionContext ctx = getTransactionContext();
        if (ctx == null) { 
            ctx = new TransactionContext(this);
            transactionContext.set(ctx);
        }
        ctx.beginTransaction(transId, ++maxTransSeqNo);
    }

    /**
     * Commit transaction. 
     * @exception ConflictExpetion is thrown if object modified by this transaction was also changed by some other 
     * previously committed transaction 
     * @exception NotUniqueException is thrown if indexable field with unique constraint of inserted or updated object 
     * contains the same value as current version of some other instance of this class (committed by another transact).
     * Please notice that CDatabase is not able to detect unique constraint violations within one transaction - if it
     * inserts two instances with same value of indexable field.
     * @return identifier assigned to this transaction or 0 if there is no active transaction. 
     * This identifier can be used to obtain snapshot of the database at the moment of this transaction execution.
     */
    public long commitTransaction() throws ConflictException, NotUniqueException 
    { 
        TransactionContext ctx = getTransactionContext();
        if (ctx == null || ctx.transId == TransactionContext.IMPLICIT_TRANSACTION_ID) { 
            return 0;
        }
        if (ctx.isEmptyTransaction() && !root.isModified()) { 
            ctx.endTransaction();
            return ctx.transId;
        }
        root.exclusiveLock();
        try { 
            Collection<CVersion> workSet = ctx.getWorkingCopies();
            long transId = ctx.transId;
            for (CVersion v : workSet) {
                if ((v.flags & CVersion.NEW) == 0 && v.history.getLast().transId > transId) { 
                    throw new ConflictException(v);
                }
                TableDescriptor desc = lookupTable(v.getClass());
                if (desc != null) { 
                    desc.checkConstraints(v);
                }
            }
            if (ctx.seqNo == minTransSeqNo) { 
                minTransSeqNo += 1;
                if (ctx.transId > lastActiveTransId) { 
                    lastActiveTransId = ctx.transId;
                }
            }
            transId = root.newTransactionId();
            for (CVersion v : workSet) { 
                CVersionHistory vh = v.history;
                TableDescriptor desc = getTable(v.getClass());
                int flags = v.flags;

                v.transId = transId;
                v.id = vh.getNumberOfVersions() + 1;
                v.flags &= ~(CVersion.WORKING_COPY|CVersion.NEW);
                v.date = new Date();
                vh.add(v);

                if ((flags & (CVersion.NEW|CVersion.DELETED)) == CVersion.NEW) { 
                    desc.classExtent.add(vh);
                } else if (vh.limited) { 
                    CVersion old;
                    while ((old = vh.get(CVersion.FIRST_VERSION_ID)).transId < lastActiveTransId) {
                        desc.excludeFromIndices(old);
                        excludeFromFullTextIndex(desc, old);
                        vh.remove(CVersion.FIRST_VERSION_ID);
                        old.deallocate();
                    }
                }
                if ((flags & CVersion.DELETED) == 0) {
                    desc.includeInIndices(v); 
                    includeInFullTextIndex(desc.buildDocument(v));
                }
            }      
            try {
                if (indexWriter != null) { 
                    indexWriter.close();
                    indexWriter = null;
                }
            } catch (IOException x) { 
                throw new IOError(x);
            }
            storage.commit();
            return transId;
        } finally { 
            root.unlock();         
            ctx.endTransaction();
        }
    }
    
    /**
     * Rollback transaction
     */
    public void rollbackTransaction() 
    { 
        TransactionContext ctx = getTransactionContext();
        if (ctx != null) { 
            root.exclusiveLock();
            try { 
                if (ctx.seqNo == minTransSeqNo) { 
                    minTransSeqNo += 1;
                    if (ctx.transId > lastActiveTransId) { 
                        lastActiveTransId = ctx.transId;
                    }
                }
            } finally { 
                root.unlock();         
                ctx.endTransaction();
            }
        }
    }

    /**
     * Insert new record in the database
     * @param record inserted record
     * @exception ObjectAlreadyInsertedException when object is part of some other version history
     * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
     */
    public <T extends CVersion> void insert(T record) 
    { 
        getWriteTransactionContext().insert(record);
    }

    /**
     * Update the record. 
     * @param record updated record
     * @return working copy of the specified version
     * @exception AmbiguousVersionException when some other version from the same version history was already updated by the current transaction
     * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
     */
    public <T extends CVersion> T update(T record) { 
        return (T)record.update();
    }

    /**
     * Mark current version as been deleted.
     * @param record working copy of the current version or current version itself. In the last case work copy is created
     * @exception NotCurrentVersionException is thrown if specified version is not current in the version history
     * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
     */
    public void delete(CVersion record) { 
        record.delete();
    }

    /**
     * Extract single result from the returned result set.
     * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
     * <code>
     *    MyClass obj = db.getSingleton(db.find(MyClass.class, "key", value));
     * </code>
     * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
     * methods
     * @return selected object if result set contains exactly one object or null if result set is empty
     * @exception SingletonException if result set contains more than one element
     */     
    public <T extends CVersion> T getSingleton(Iterator<T> iterator) throws SingletonException
    { 
        root.sharedLock();
        try { 
            T obj = null;
            if (iterator.hasNext()) { 
                obj = iterator.next();
                if (iterator.hasNext()) {  
                    throw new SingletonException();
                }
            }
            return obj;
        } finally { 
            root.unlock();
        } 
    }

    /**
     * Enumeration specifying version sort order in the result set.
     * Sorting is performed by version identifier
     */
    public enum VersionSortOrder 
    {
        ASCENT(1),
        NONE(0),
        DESCENT(-1);

        int delta;

        VersionSortOrder(int delta) { 
            this.delta = delta;
        }        
    }    

    /**
     * Convert returned result set to the List collection. 
     * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
     * <code>
     *    List<MyClass> list = db.toList(db.select(MyClass.class, "price between 100 and 1000 and amount > 100"));
     * </code>
     * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very 
     * fast but fetching each element requires some additional calculations. 
     * Using this method you can force loading of all result set elements. So you will know
     * precise number of selected records, can access them in any order and without extra overhead.
     * The payment for these advantages is increased time of query execution and amount of memory needed
     * to hold all selected objects.
     * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
     * methods
     * @return List containing all selected objects
     */     
    public <T extends CVersion> List<T> toList(Iterator<T> iterator) 
    { 
        return toList(iterator, Integer.MAX_VALUE);
    }

    /**
     * Converted returned result set to List collection with limit for number of fetched records. 
     * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
     * <code>
     *    List<MyClass> list = db.toList(db.select(MyClass.class, "price between 100 and 1000 and amount > 100"));
     * </code>
     * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very 
     * fast but fetching each element requires some additional calculations. 
     * Using this method you can force loading of all result set elements. So you will know
     * precise number of selected results, can access them in any order and without extra overhead.
     * The payment for these advantages is increased time of query execution and amount of memory needed
     * to hold all selected objects.
     * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
     * methods
     * @param limit result list limit
     * @return List containing selected objects, if number of selected objects is larger than specified <code>limit</code>, 
     * then only first <code>limit</code> of them will be placed in the list and other will be ignored.
     */         
    public <T extends CVersion> List<T> toList(Iterator<T> iterator, int limit)
    { 
        ArrayList<T> list = new ArrayList();
        root.sharedLock();
        try { 
            while (--limit >= 0 && iterator.hasNext()) { 
                list.add(iterator.next());
            } 
            return list;
        } finally { 
            root.unlock();
        } 
    }

    /**
     * Converted returned result set to array.
     * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
     * <code>
     *    MyClass[] arr = db.toArray(new MyClass[0], db.select(MyClass.class, "price between 100 and 1000 and amount > 100", 
     *                                                         VersionSortOrder.ASCENT));
     * </code>
     * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very 
     * fast but fetching each element requires some additional calculations. 
     * Using this method you can force loading of all result set elements. So you will know
     * precise number of selected results, can access them in any order and without extra overhead.
     * The payment for these advantages is increased time of query execution and amount of memory needed
     * to hold all selected objects.
     * Specifying sort order allows for example to select the first/last version satisfying search criteria.
     * @param arr the array into which the elements of the list are to be stored, 
     * if it is big enough; otherwise, a new array of the same runtime type is allocated for this purpose.
     * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
     * methods
     * @param order version sort order: ASCENT, DESCENT or NONE. Array elements will be sorted by version identifiers. 
     * If sort order is not specified NONE (then records in the result array will be in the same order as returned by the iterator)
     * @return an array containing selected objects in the given order
     */         
    public <T extends CVersion> T[] toArray(T[] arr, Iterator<T> iterator, VersionSortOrder order) 
    { 
        ArrayList<T> list = new ArrayList();
        root.sharedLock();
        try { 
            while (iterator.hasNext()) { 
                list.add(iterator.next());
            }
        } finally { 
            root.unlock();
        }
        arr = list.toArray(arr);
        if (order != VersionSortOrder.NONE) { 
            final int delta = order.delta;
            Arrays.sort(arr, new Comparator<T>() { 
                            public int compare(T v1, T v2) { 
                                return v1.transId < v2.transId ? -delta : v1.transId == v2.transId ? 0 : delta;
                            }
                        });
        }
        return arr;
    }
    
    /**
     * Select current version of records from specified table matching specified criteria
     * @param table class corresponding to the table
     * @param predicate search predicate
     * @return iterator through selected records. This iterator doesn't support remove() method.
     * If there are no instances of such class in the database, then empty iterator is returned
     * @exception CompileError exception is thrown if predicate is not valid JSQL exception
     * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
     */
    public <T extends CVersion> IterableIterator<T> select(Class table, String predicate) 
    { 
        return select(table, predicate, VersionSelector.CURRENT);
    }
        
    /**
     * Select records from the specified table using version selector
     * @param table class corresponding to the table
     * @param predicate search predicate
     * @param selector version selector
     * @return iterator through selected records. This iterator doesn't support remove() method.
     * If there are no instances of such class in the database, then empty iterator is returned
     * @exception CompileError exception is thrown if predicate is not valid JSQL exception
     * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
     */
    public <T extends CVersion> IterableIterator<T> select(Class table, String predicate, VersionSelector selector)
    { 
        Query q = prepare(table, predicate);
        return q.execute(getRecords(table, selector));
    }

    /**
     * Prepare JSQL query. Prepare is needed for queries with parameters. Also
     * preparing query can improve speed if query will be executed multiple times
     * (using prepare, it is compiled only once).
     * To execute prepared query, you should use Query.execute(db.getRecords(TABLE.class)) method
     * @param table class corresponding to the table
     * @param predicate search predicate
     * @return prepared query
     * @exception CompileError exception is thrown if predicate is not valid JSQL exception
     */
    public <T extends CVersion> Query<T> prepare(Class table, String predicate) 
    { 
        return prepare(table, predicate, VersionSelector.CURRENT);
    }

    /**
     * Prepare JSQL query. Prepare is needed for queries with parameters. Also
     * preparing query can improve speed if query will be executed multiple times
     * (using prepare, it is compiled only once).
     * To execute prepared query, you should use Query.execute(db.getRecords(TABLE.class)) method
     * @param table class corresponding to the table
     * @param predicate search predicate
     * @param selector version selector
     * @return prepared query
     * @exception CompileError exception is thrown if predicate is not valid JSQL exception
     */
    public <T extends CVersion> Query<T> prepare(Class table, String predicate, VersionSelector selector) 
    { 
        Query q = storage.createQuery();
        q.prepare(table, predicate);            
        TableDescriptor desc = lookupTable(table);
        if (desc != null) { 
            desc.registerIndices(q, root, selector);
        }
        return q;
    }

    /** 
     * Get iterator through current version of all table records
     * @param table class corresponding to the table
     * @return iterator through all table records. If there are no instances of such class in the database, then empty
     * iterator is returned
     */
    public <T extends CVersion> IterableIterator<T> getRecords(Class table)
    {
        return getRecords(table, VersionSelector.CURRENT);
    }

    /** 
     * Get iterator through all records of the table using specified version selector
     * @param table class corresponding to the table
     * @param selector version selector
     * @return iterator through all table records. If there are no instances of such class in the database, then empty
     * iterator is returned
     */
    public <T extends CVersion> IterableIterator<T> getRecords(Class table, VersionSelector selector)
    {         
        root.sharedLock();
        try { 
            TableDescriptor desc = lookupTable(table);        
            IterableIterator<T> iterator = (desc == null) 
                ? new EmptyIterator<T>() 
                : new ExtentIterator<T>(desc.iterator(), root, selector);
            return iterator;
        } finally { 
            root.unlock();
        } 
    }

    /**
     * Select current version from specified table by key
     * @param table class corresponding to the table
     * @param field indexed field
     * @param key key value
     * @return iterator through selected records. This iterator doesn't support remove() method.
     * If there are no instances of such class in the database, then empty iterator is returned
     * @exception NoSuchIndexException if there is no index for the specified field
     */
    public <T extends CVersion> IterableIterator<T> find(Class table, String field, Key key) throws NoSuchIndexException 
    {
        return find(table, field, key, VersionSelector.CURRENT);
    }

    /**
     * Select records from the specified table by key using version selector
     * @param table class corresponding to the table
     * @param field indexed field
     * @param key key value
     * @param selector version selector
     * @return iterator through selected records. This iterator doesn't support remove() method.
     * If there are no instances of such class in the database, then empty iterator is returned
     * @exception NoSuchIndexException if there is no index for the specified field
     */
    public <T extends CVersion> IterableIterator<T> find(Class table, String field, Key key, VersionSelector selector) throws NoSuchIndexException 
    {
        TableDescriptor desc = lookupTable(table);
        if (desc == null) { 
            return new EmptyIterator<T>();
        }
        TableDescriptor.IndexDescriptor idesc = desc.findIndex(field);
        if (idesc == null) { 
            throw new NoSuchIndexException(field);
        }
        key = idesc.checkKey(key);
        return new IndexFilter<T>(idesc.index, root, selector).iterator(key, key, GenericIndex.ASCENT_ORDER);
    }

    /**
     * Select records from the specified table by key range using version selector
     * @param table class corresponding to the table
     * @param field indexed field
     * @param min minimal key value
     * @param max maximal key value
     * @param selector version selector
     * @param order key sort order: GenericIndex.ASCENT_ORDER or GenericIndex.DESCENT_ORDER
     * @return iterator through selected records. This iterator doesn't support remove() method.
     * If there are no instances of such class in the database, then empty iterator is returned
     * @exception NoSuchIndexException if there is no index for the specified field
     */
    public <T extends CVersion> IterableIterator<T> find(Class table, String field, Key min, Key max, VersionSelector selector, int order)
    {
        TableDescriptor desc = lookupTable(table);
        if (desc == null) { 
            return new EmptyIterator<T>();
        }
        TableDescriptor.IndexDescriptor idesc = desc.findIndex(field);
        if (idesc == null) { 
            throw new NoSuchIndexException(field);
        }
        return new IndexFilter<T>(idesc.index, root, selector).iterator(idesc.checkKey(min), idesc.checkKey(max), order);
    }

    /**
     * Perform full text search through the current version of all objects in the database
     * @param query full text search search query. It should be compliant with Lucene query syntax and may
     * refer to the particular fields or to any full text searchable field:
     * <ul>
     * <li>"name:John" - select document with field "name" containing word "John"</li>
     * <li>"title:magic AND author:Clark" - select document with field "title" containing word "magic" and field "author"
     *                                     containing word "Clark"</li>
     * <li>"atomic nuclear power" - select any document which full text searchable fields contain any of the specified words</li>
     * <li>"Class:com.eshop.Player AND description:DivX" - select objects with class com.eshop.Player which description contains word "DivX"</li>
     * </ul>
     * @param limit search result limit 
     * @return array with matched documents
     * @exception FullTextSearchQuerySyntaxError if query syntax is invalid
     */
    public FullTextSearchResult[] fullTextSearch(String query, int limit)
    {
        return fullTextSearch(query, limit, VersionSelector.CURRENT, VersionSortOrder.NONE);
    }

    /**
     * Perform full text search using version selector
     * @param query full text search search query. It should be compliant with Lucene query syntax and may
     * refer to the particular fields or to any full text searchable field:
     * <ul>
     * <li>"name:John" - select document with field "name" containing word "John"</li>
     * <li>"title:magic AND author:Clark" - select document with field "title" containing word "magic" and field "author"
     *                                     containing word "Clark"</li>
     * <li>"atomic nuclear power" - select any document which full text searchable fields contain any of the specified words</li>
     * <li>"Class:com.eshop.Player AND description:DivX" - select objects with class com.eshop.Player which description contains word "DivX"</li>
     * </ul>
     * @param limit search result limit 
     * @param selector version selector
     * @param order result set versions sort order (if VersionSortOrder.NONE then order of documents returned by Lucene 
     * is preserved - documents are sorted by score)
     * @return array with matched documents
     * @exception FullTextSearchQuerySyntaxError if query syntax is invalid
     */
    public FullTextSearchResult[] fullTextSearch(String query, int limit, VersionSelector selector, VersionSortOrder order)
    {
        ArrayList<FullTextSearchResult> result = null;
        root.sharedLock();
        try { 
            Hits hits;
            try { 
                IndexSearcher searcher = new IndexSearcher(getIndexReader());
                if (selector.kind == VersionSelector.Kind.TimeSlice) { 
                    long from = selector.from != null ? selector.from.getTime() : 0;
                    long till = selector.till != null ? selector.till.getTime() : System.currentTimeMillis();
                    StringBuilder sb = new StringBuilder("Created:[");
                    sb.append(DateTools.timeToString(from, DateTools.Resolution.MINUTE));
                    sb.append(" TO ");
                    sb.append(DateTools.timeToString(till, DateTools.Resolution.MINUTE));
                    sb.append("] AND (");
                    sb.append(query);
                    sb.append(')');
                    query = sb.toString();
                }
                
                QueryParser parser = new QueryParser("Any", analyzer);
                hits = searcher.search(parser.parse(query));
            } catch (IOException x) {
                throw new IOError(x);
            } catch (ParseException x) { 
                throw new FullTextSearchQuerySyntaxError(x);
            } 
            result = new ArrayList<FullTextSearchResult>();
            Iterator<FullTextSearchResult> iterator = new FullTextSearchIterator(hits, storage, selector);
            while (--limit >= 0 && iterator.hasNext()) { 
                result.add(iterator.next());
            }
        } finally { 
            root.unlock();
        } 
        FullTextSearchResult[] arr = result.toArray(new FullTextSearchResult[result.size()]);
        if (order != VersionSortOrder.NONE) { 
            final int delta = order.delta;
            Arrays.sort(arr, new Comparator<FullTextSearchResult>() { 
                            public int compare(FullTextSearchResult r1, FullTextSearchResult r2) { 
                                long t1 = r1.getVersion().transId;
                                long t2 = r2.getVersion().transId;
                                return t1 < t2 ? -delta : t1 == t2 ? 0 : delta;
                            }
                        });
        }
        return arr;
    }

    /**
     * Restore full text search index.
     * If the full text search index is located in file system directory it may be get out of sync with the Perst database or even be 
     * corrupted in case of failure. In this case index can be reconstructed using this method.
     */
    public synchronized void restoreFullTextIndex()
    {
        root.exclusiveLock();
        try { 
            try { 
                if (indexWriter != null) { 
                    indexWriter.close();
                }
                if (indexReader != null) { 
                    indexReader.close();
                    indexReader = null;
                }
                indexWriter = new IndexWriter(dir, analyzer, true);
                for (TableDescriptor table : root.tables) {
                    for (CVersionHistory<?> vh : table) {
                        for (CVersion v : vh) { 
                            if (!v.isDeletedVersion()) { 
                                Document doc = table.buildDocument(v);
                                if (doc != null) { 
                                    indexWriter.addDocument(doc);
                                }
                            }
                        }
                    }
                }                                       
                indexWriter.optimize();
            } catch (IOException x) {
                throw new IOError(x);
            }
        } finally { 
            root.unlock();
        }
    }

    /**
     * Optimize full text index. This method can be periodically called by application (preferably in idle time)
     * to optimize full text search index
     */
    public synchronized void optimizeFullTextIndex() 
    {
        root.exclusiveLock();
        try { 
            try { 
                getIndexWriter().optimize();
            } catch (IOException x) {
                throw new IOError(x);
            }
        } finally { 
            root.unlock();
        }
    }
        

    /**
     * Get storage associated with this database
     * @return underlying storage
     */
    public Storage getStorage() 
    { 
        return storage;
    }


    /**
     * Get resource used to synchronize access to the database
     */
    public IResource getResource() 
    { 
        return root;
    }

    /**
     * Get user data. Continuous uses Perst root objects for its own purposes. 
     * But this methods allows application to specify its own root and store in the Perst 
     * storage non-versioned objects. 
     * @return reference to the persistent object previously stored by setUserData()
     */
    public IPersistent getUserData() 
    { 
        root.sharedLock();
        try { 
            return root.userData;
        } finally { 
            root.unlock();
        }
    }

     /**
     * Set user data. Continuous uses Perst root objects for its own purposes. 
     * But this methods allows application to specify its own root and store in the Perst 
     * storage non-versioned objects. 
     * @param obj reference to the persistent object which will be stored in Perst root object
     */
    public void setUserData(IPersistent obj) 
    { 
        root.exclusiveLock();
        try { 
            root.userData = obj;
            root.modify();
        } finally { 
            root.unlock();
        }
    }

    /**
     * Static instance of the database which can be used by application working with the single database
     */
    public static CDatabase instance = new CDatabase();


    static TransactionContext getTransactionContext() 
    { 
        return transactionContext.get();
    }

    static TransactionContext getWriteTransactionContext() 
    { 
        TransactionContext ctx = transactionContext.get();
        if (ctx == null) { 
            throw new TransactionNotStartedException();
        }            
        return ctx;
    }

    void openFullTextIndex(RootObject root, String path)
    {
        boolean create;
        if (path == null) { 
            // Store Lucene index in Perst database
            create = root.catalogue == null;
            dir = new PerstDirectory(root);
        } else { 
            File file = new File(path);
            create = !file.exists();
            try { 
                dir = FSDirectory.getDirectory(path);
            } catch (IOException x) {
                throw new IOError(x);
            }
        }
        analyzer = new StandardAnalyzer();
        try { 
            indexWriter = new IndexWriter(dir, analyzer, create);
        } catch (IOException x) {
            throw new IOError(x);
        }
    }

    void excludeFromFullTextIndex(TableDescriptor desc, CVersion v) 
    {
        if (desc.isFullTextSearchable()) { 
            Term term = new Term("Oid", Integer.toString(v.getOid()));
            try { 
                getIndexReader().deleteDocuments(term);
            } catch (IOException x) { 
                throw new IOError(x);
            }
        }
    }
    

    void includeInFullTextIndex(Document doc)
    {
        if (doc != null) { 
            try { 
                getIndexWriter().addDocument(doc);
            } catch (IOException x) {
                throw new IOError(x);
            }
        }
    }

    synchronized TableDescriptor lookupTable(Class type)
    {
        return typeMap.get(type);
    }

    synchronized TableDescriptor getTable(Class type)
    {
        TableDescriptor desc = typeMap.get(type);
        if (desc == null) { 
            desc = new TableDescriptor(storage, type);
            typeMap.put(type, desc);
            root.tables.add(desc);
        }
        return desc;
    }

    synchronized IndexWriter getIndexWriter() throws IOException 
    {
        if (indexWriter == null) { 
            if (indexReader != null) { 
                indexReader.close();
                indexReader = null;
            }
            indexWriter = new IndexWriter(dir, analyzer, false);
        }
        return indexWriter;
    }

    synchronized IndexReader getIndexReader() throws IOException 
    {             
        if (indexReader == null) { 
            if (indexWriter != null) { 
                indexWriter.close();
                indexWriter = null;
            }
            indexReader = IndexReader.open(dir);
        }
        return indexReader;
    }

    static ThreadLocal<TransactionContext> transactionContext = new ThreadLocal<TransactionContext>();

    Storage storage;
    RootObject root;
    HashMap<Class,TableDescriptor> typeMap;   
    IndexWriter indexWriter;
    IndexReader indexReader;
    StandardAnalyzer analyzer;
    Directory dir;
    long minTransSeqNo;
    long maxTransSeqNo;
    long lastActiveTransId;
}
java2s.com  | Contact Us | Privacy Policy
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.