001: package org.garret.perst.continuous;
002:
003: import java.util.*;
004: import java.io.*;
005: import org.garret.perst.*;
006: import org.garret.perst.impl.ClassDescriptor;
007:
008: import org.apache.lucene.index.Term;
009: import org.apache.lucene.index.IndexWriter;
010: import org.apache.lucene.index.IndexReader;
011: import org.apache.lucene.search.IndexSearcher;
012: import org.apache.lucene.document.Document;
013: import org.apache.lucene.document.DateTools;
014: import org.apache.lucene.store.FSDirectory;
015: import org.apache.lucene.store.Directory;
016: import org.apache.lucene.queryParser.*;
017: import org.apache.lucene.search.Hits;
018: import org.apache.lucene.analysis.standard.StandardAnalyzer;
019:
020: /**
021: * <p>
022: * This class emulates relational database on top of Perst storage
023: * It maintains class extends and associated indices. Is supports JSQL and full text search queries.
024: * This class provides version control system, optimistic access control and full text search (using Lucene).
025: * </p><p>
026: * All transactions are isolated from each other and do not see changes made by other transactions
027: * (even committed transactions if commit happens after start of this transaction).
028: * The database keeps all versions of the object hich are grouped in version history.
029: * Version history is linear which means that no branches in version tree are possible.
030: * When transaction tries to update some object then working copy of the current version is created.
031: * Only this copy is changed, leaving all other versions unchanged. This modification will be visible only
032: * for the current transaction. Created or modified objects are linked in indices when transaction is committed.
033: * It means that application will not be able to find in the index just inserted or updated object. It has
034: * first to commit the current transaction. All working copies created during transaction execution
035: * are linked in he list stored in transaction context which is in turn associated with the current thread.
036: * It means two things:
037: * <ol>
038: * <li>one thread can participate in only one transaction and one transaction can be controlled only by one thread</li>
039: * <li>size of transaction is limited by the amount of memory available for the application</li>
040: * </ol>
041: * When transaction is committed, then all its working copies are inspected. If the last version in version history is
042: * not equal to one from which working copy was created then conflict is detected and ConflictException is thrown.
043: * And if insertion of working copy in index will cause unique constraint violation then NotUniqueException is thrown.
044: * If no conflicts are detected, then transaction is committed and each working copy becomes new version in correspondent
045: * version history.
046: * </p><p>
047: * Isolation of transaction provided by CDatabase class is based on multiversioning and modification of working copies
048: * instead of original objects. Working copy is produced from the current version using clone (shallow copy) method.
049: * Clone will work correctly if components of the objects have primitive type (int, double,...) or are immutable (String).
050: * Using java.util.Date type is possible only if you do not try to update its components.
051: * Instead of normal Java references, you should use references to correspondent CVersionHistory (it allows to preserve
052: * reference consistency if new version of reference object is created).
053: * CDatabase class also supports links (components with Link type) and arrays.
054: * Fields with <i>value type</i> (class implementing IValue interface) can be used only if them are
055: * immutable or cloneable (implement org.garret.perst.ICloneable interface).
056: * Using any other data type including Perst collection classes is not supported and can cause unpredictable behavior.
057: */
058: public class CDatabase {
059: /**
060: * Open the database. This method initialize database if it not initialized yet.
061: * @param storage opened storage. Storage should be either empty (non-initialized, either
062: * previously initialized by the this method. It is not possible to open storage with
063: * root object other than RootObject created by this method.
064: * @param fullTextIndexPath path to the directory containing Lucene database. If null, then Lucene index will be stored
065: * inside the main Perst storage.
066: */
067: public void open(Storage storage, String fullTextIndexPath) {
068: this .storage = storage;
069: storage.setProperty("perst.concurrent.iterator", Boolean.TRUE);
070: root = (RootObject) storage.getRoot();
071: typeMap = new HashMap<Class, TableDescriptor>();
072: if (root == null) {
073: root = new RootObject(storage);
074: storage.setRoot(root);
075: } else {
076: for (TableDescriptor desc : root.tables) {
077: typeMap.put(desc.type, desc);
078: }
079: }
080: openFullTextIndex(root, fullTextIndexPath);
081: storage.commit();
082: }
083:
084: /**
085: * Close the database. All uncommitted transaction will be aborted.
086: * Close full text index and storage.
087: */
088: public void close() {
089: try {
090: if (indexWriter != null) {
091: indexWriter.close();
092: indexWriter = null;
093: }
094: if (indexReader != null) {
095: indexReader.close();
096: indexReader = null;
097: }
098: if (dir != null) {
099: dir.close();
100: dir = null;
101: }
102: } catch (IOException x) {
103: throw new IOError(x);
104: }
105: storage.close();
106: storage = null;
107:
108: root = null;
109: typeMap = null;
110: analyzer = null;
111: }
112:
113: /**
114: * Start new transaction. All changes can be made only within transaction context.
115: * Transaction context is associated with thread. It means that one thread can run only one transaction and
116: * one transaction can be handled only by one thread.
117: * Transaction is observing database state at the moment of the transaction start. Any changes done by
118: * other transactions (committed and uncommitted) after this moment are not visible for the current transaction.
119: * CDatabase uses optimistic access control. It means that transaction may be aborted if conflict
120: * is detected during transaction commit.
121: */
122: public void beginTransaction() {
123: beginTransaction(root.getLastTransactionId());
124: }
125:
126: /**
127: * Start transaction with the given identifier.
128: * Identifier of the transaction can be obtained using CVersion.getTransactionId() method.
129: * Starting transaction with ID of previously committed transaction allows to obtain snapshot
130: * of the database at the moment of this transaction execution. All search methods
131: * using default version selector (referencing current version) and CVersionHistory.getCurrent()
132: * method will select versions which were current at the moment of this transaction execution.
133: * @param transId identifier of previously committed transaction
134: * @exception TransactionAlreadyStartedException when thread attempts to start new transaction without committing or
135: * aborting previous one. Transaction should be explicitly finished using CDatabase.commit or CDatabase.rollback method.
136: * Each thread should start its own transaction, it is not possible to share the same transaction by more than one threads.
137: * It is possible to call beginTransaction several times without commit or rollback only if previous transaction was read-only
138: * - didn't change any object
139: */
140: public synchronized void beginTransaction(long transId) {
141: TransactionContext ctx = getTransactionContext();
142: if (ctx == null) {
143: ctx = new TransactionContext(this );
144: transactionContext.set(ctx);
145: }
146: ctx.beginTransaction(transId, ++maxTransSeqNo);
147: }
148:
149: /**
150: * Commit transaction.
151: * @exception ConflictExpetion is thrown if object modified by this transaction was also changed by some other
152: * previously committed transaction
153: * @exception NotUniqueException is thrown if indexable field with unique constraint of inserted or updated object
154: * contains the same value as current version of some other instance of this class (committed by another transact).
155: * Please notice that CDatabase is not able to detect unique constraint violations within one transaction - if it
156: * inserts two instances with same value of indexable field.
157: * @return identifier assigned to this transaction or 0 if there is no active transaction.
158: * This identifier can be used to obtain snapshot of the database at the moment of this transaction execution.
159: */
160: public long commitTransaction() throws ConflictException,
161: NotUniqueException {
162: TransactionContext ctx = getTransactionContext();
163: if (ctx == null
164: || ctx.transId == TransactionContext.IMPLICIT_TRANSACTION_ID) {
165: return 0;
166: }
167: if (ctx.isEmptyTransaction() && !root.isModified()) {
168: ctx.endTransaction();
169: return ctx.transId;
170: }
171: root.exclusiveLock();
172: try {
173: Collection<CVersion> workSet = ctx.getWorkingCopies();
174: long transId = ctx.transId;
175: for (CVersion v : workSet) {
176: if ((v.flags & CVersion.NEW) == 0
177: && v.history.getLast().transId > transId) {
178: throw new ConflictException(v);
179: }
180: TableDescriptor desc = lookupTable(v.getClass());
181: if (desc != null) {
182: desc.checkConstraints(v);
183: }
184: }
185: if (ctx.seqNo == minTransSeqNo) {
186: minTransSeqNo += 1;
187: if (ctx.transId > lastActiveTransId) {
188: lastActiveTransId = ctx.transId;
189: }
190: }
191: transId = root.newTransactionId();
192: for (CVersion v : workSet) {
193: CVersionHistory vh = v.history;
194: TableDescriptor desc = getTable(v.getClass());
195: int flags = v.flags;
196:
197: v.transId = transId;
198: v.id = vh.getNumberOfVersions() + 1;
199: v.flags &= ~(CVersion.WORKING_COPY | CVersion.NEW);
200: v.date = new Date();
201: vh.add(v);
202:
203: if ((flags & (CVersion.NEW | CVersion.DELETED)) == CVersion.NEW) {
204: desc.classExtent.add(vh);
205: } else if (vh.limited) {
206: CVersion old;
207: while ((old = vh.get(CVersion.FIRST_VERSION_ID)).transId < lastActiveTransId) {
208: desc.excludeFromIndices(old);
209: excludeFromFullTextIndex(desc, old);
210: vh.remove(CVersion.FIRST_VERSION_ID);
211: old.deallocate();
212: }
213: }
214: if ((flags & CVersion.DELETED) == 0) {
215: desc.includeInIndices(v);
216: includeInFullTextIndex(desc.buildDocument(v));
217: }
218: }
219: try {
220: if (indexWriter != null) {
221: indexWriter.close();
222: indexWriter = null;
223: }
224: } catch (IOException x) {
225: throw new IOError(x);
226: }
227: storage.commit();
228: return transId;
229: } finally {
230: root.unlock();
231: ctx.endTransaction();
232: }
233: }
234:
235: /**
236: * Rollback transaction
237: */
238: public void rollbackTransaction() {
239: TransactionContext ctx = getTransactionContext();
240: if (ctx != null) {
241: root.exclusiveLock();
242: try {
243: if (ctx.seqNo == minTransSeqNo) {
244: minTransSeqNo += 1;
245: if (ctx.transId > lastActiveTransId) {
246: lastActiveTransId = ctx.transId;
247: }
248: }
249: } finally {
250: root.unlock();
251: ctx.endTransaction();
252: }
253: }
254: }
255:
256: /**
257: * Insert new record in the database
258: * @param record inserted record
259: * @exception ObjectAlreadyInsertedException when object is part of some other version history
260: * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
261: */
262: public <T extends CVersion> void insert(T record) {
263: getWriteTransactionContext().insert(record);
264: }
265:
266: /**
267: * Update the record.
268: * @param record updated record
269: * @return working copy of the specified version
270: * @exception AmbiguousVersionException when some other version from the same version history was already updated by the current transaction
271: * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
272: */
273: public <T extends CVersion> T update(T record) {
274: return (T) record.update();
275: }
276:
277: /**
278: * Mark current version as been deleted.
279: * @param record working copy of the current version or current version itself. In the last case work copy is created
280: * @exception NotCurrentVersionException is thrown if specified version is not current in the version history
281: * @exception TransactionNotStartedException if transaction was not started by this thread using CDatabase.beginTransaction
282: */
283: public void delete(CVersion record) {
284: record.delete();
285: }
286:
287: /**
288: * Extract single result from the returned result set.
289: * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
290: * <code>
291: * MyClass obj = db.getSingleton(db.find(MyClass.class, "key", value));
292: * </code>
293: * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
294: * methods
295: * @return selected object if result set contains exactly one object or null if result set is empty
296: * @exception SingletonException if result set contains more than one element
297: */
298: public <T extends CVersion> T getSingleton(Iterator<T> iterator)
299: throws SingletonException {
300: root.sharedLock();
301: try {
302: T obj = null;
303: if (iterator.hasNext()) {
304: obj = iterator.next();
305: if (iterator.hasNext()) {
306: throw new SingletonException();
307: }
308: }
309: return obj;
310: } finally {
311: root.unlock();
312: }
313: }
314:
315: /**
316: * Enumeration specifying version sort order in the result set.
317: * Sorting is performed by version identifier
318: */
319: public enum VersionSortOrder {
320: ASCENT(1), NONE(0), DESCENT(-1);
321:
322: int delta;
323:
324: VersionSortOrder(int delta) {
325: this .delta = delta;
326: }
327: }
328:
329: /**
330: * Convert returned result set to the List collection.
331: * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
332: * <code>
333: * List<MyClass> list = db.toList(db.select(MyClass.class, "price between 100 and 1000 and amount > 100"));
334: * </code>
335: * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very
336: * fast but fetching each element requires some additional calculations.
337: * Using this method you can force loading of all result set elements. So you will know
338: * precise number of selected records, can access them in any order and without extra overhead.
339: * The payment for these advantages is increased time of query execution and amount of memory needed
340: * to hold all selected objects.
341: * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
342: * methods
343: * @return List containing all selected objects
344: */
345: public <T extends CVersion> List<T> toList(Iterator<T> iterator) {
346: return toList(iterator, Integer.MAX_VALUE);
347: }
348:
349: /**
350: * Converted returned result set to List collection with limit for number of fetched records.
351: * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
352: * <code>
353: * List<MyClass> list = db.toList(db.select(MyClass.class, "price between 100 and 1000 and amount > 100"));
354: * </code>
355: * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very
356: * fast but fetching each element requires some additional calculations.
357: * Using this method you can force loading of all result set elements. So you will know
358: * precise number of selected results, can access them in any order and without extra overhead.
359: * The payment for these advantages is increased time of query execution and amount of memory needed
360: * to hold all selected objects.
361: * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
362: * methods
363: * @param limit result list limit
364: * @return List containing selected objects, if number of selected objects is larger than specified <code>limit</code>,
365: * then only first <code>limit</code> of them will be placed in the list and other will be ignored.
366: */
367: public <T extends CVersion> List<T> toList(Iterator<T> iterator,
368: int limit) {
369: ArrayList<T> list = new ArrayList();
370: root.sharedLock();
371: try {
372: while (--limit >= 0 && iterator.hasNext()) {
373: list.add(iterator.next());
374: }
375: return list;
376: } finally {
377: root.unlock();
378: }
379: }
380:
381: /**
382: * Converted returned result set to array.
383: * It should be applied as pipeline to CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute methods:
384: * <code>
385: * MyClass[] arr = db.toArray(new MyClass[0], db.select(MyClass.class, "price between 100 and 1000 and amount > 100",
386: * VersionSortOrder.ASCENT));
387: * </code>
388: * Result set iterator usually selects records in Perst in lazy mode, so execution of query is very
389: * fast but fetching each element requires some additional calculations.
390: * Using this method you can force loading of all result set elements. So you will know
391: * precise number of selected results, can access them in any order and without extra overhead.
392: * The payment for these advantages is increased time of query execution and amount of memory needed
393: * to hold all selected objects.
394: * Specifying sort order allows for example to select the first/last version satisfying search criteria.
395: * @param arr the array into which the elements of the list are to be stored,
396: * if it is big enough; otherwise, a new array of the same runtime type is allocated for this purpose.
397: * @param iterator result set iterator returned by CDatabase.select/CDatabase.find/CDatabase.getRecords/Query.execute
398: * methods
399: * @param order version sort order: ASCENT, DESCENT or NONE. Array elements will be sorted by version identifiers.
400: * If sort order is not specified NONE (then records in the result array will be in the same order as returned by the iterator)
401: * @return an array containing selected objects in the given order
402: */
403: public <T extends CVersion> T[] toArray(T[] arr,
404: Iterator<T> iterator, VersionSortOrder order) {
405: ArrayList<T> list = new ArrayList();
406: root.sharedLock();
407: try {
408: while (iterator.hasNext()) {
409: list.add(iterator.next());
410: }
411: } finally {
412: root.unlock();
413: }
414: arr = list.toArray(arr);
415: if (order != VersionSortOrder.NONE) {
416: final int delta = order.delta;
417: Arrays.sort(arr, new Comparator<T>() {
418: public int compare(T v1, T v2) {
419: return v1.transId < v2.transId ? -delta
420: : v1.transId == v2.transId ? 0 : delta;
421: }
422: });
423: }
424: return arr;
425: }
426:
427: /**
428: * Select current version of records from specified table matching specified criteria
429: * @param table class corresponding to the table
430: * @param predicate search predicate
431: * @return iterator through selected records. This iterator doesn't support remove() method.
432: * If there are no instances of such class in the database, then empty iterator is returned
433: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
434: * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
435: */
436: public <T extends CVersion> IterableIterator<T> select(Class table,
437: String predicate) {
438: return select(table, predicate, VersionSelector.CURRENT);
439: }
440:
441: /**
442: * Select records from the specified table using version selector
443: * @param table class corresponding to the table
444: * @param predicate search predicate
445: * @param selector version selector
446: * @return iterator through selected records. This iterator doesn't support remove() method.
447: * If there are no instances of such class in the database, then empty iterator is returned
448: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
449: * @exception JSQLRuntimeException exception is thrown if there is runtime error during query execution
450: */
451: public <T extends CVersion> IterableIterator<T> select(Class table,
452: String predicate, VersionSelector selector) {
453: Query q = prepare(table, predicate);
454: return q.execute(getRecords(table, selector));
455: }
456:
457: /**
458: * Prepare JSQL query. Prepare is needed for queries with parameters. Also
459: * preparing query can improve speed if query will be executed multiple times
460: * (using prepare, it is compiled only once).
461: * To execute prepared query, you should use Query.execute(db.getRecords(TABLE.class)) method
462: * @param table class corresponding to the table
463: * @param predicate search predicate
464: * @return prepared query
465: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
466: */
467: public <T extends CVersion> Query<T> prepare(Class table,
468: String predicate) {
469: return prepare(table, predicate, VersionSelector.CURRENT);
470: }
471:
472: /**
473: * Prepare JSQL query. Prepare is needed for queries with parameters. Also
474: * preparing query can improve speed if query will be executed multiple times
475: * (using prepare, it is compiled only once).
476: * To execute prepared query, you should use Query.execute(db.getRecords(TABLE.class)) method
477: * @param table class corresponding to the table
478: * @param predicate search predicate
479: * @param selector version selector
480: * @return prepared query
481: * @exception CompileError exception is thrown if predicate is not valid JSQL exception
482: */
483: public <T extends CVersion> Query<T> prepare(Class table,
484: String predicate, VersionSelector selector) {
485: Query q = storage.createQuery();
486: q.prepare(table, predicate);
487: TableDescriptor desc = lookupTable(table);
488: if (desc != null) {
489: desc.registerIndices(q, root, selector);
490: }
491: return q;
492: }
493:
494: /**
495: * Get iterator through current version of all table records
496: * @param table class corresponding to the table
497: * @return iterator through all table records. If there are no instances of such class in the database, then empty
498: * iterator is returned
499: */
500: public <T extends CVersion> IterableIterator<T> getRecords(
501: Class table) {
502: return getRecords(table, VersionSelector.CURRENT);
503: }
504:
505: /**
506: * Get iterator through all records of the table using specified version selector
507: * @param table class corresponding to the table
508: * @param selector version selector
509: * @return iterator through all table records. If there are no instances of such class in the database, then empty
510: * iterator is returned
511: */
512: public <T extends CVersion> IterableIterator<T> getRecords(
513: Class table, VersionSelector selector) {
514: root.sharedLock();
515: try {
516: TableDescriptor desc = lookupTable(table);
517: IterableIterator<T> iterator = (desc == null) ? new EmptyIterator<T>()
518: : new ExtentIterator<T>(desc.iterator(), root,
519: selector);
520: return iterator;
521: } finally {
522: root.unlock();
523: }
524: }
525:
526: /**
527: * Select current version from specified table by key
528: * @param table class corresponding to the table
529: * @param field indexed field
530: * @param key key value
531: * @return iterator through selected records. This iterator doesn't support remove() method.
532: * If there are no instances of such class in the database, then empty iterator is returned
533: * @exception NoSuchIndexException if there is no index for the specified field
534: */
535: public <T extends CVersion> IterableIterator<T> find(Class table,
536: String field, Key key) throws NoSuchIndexException {
537: return find(table, field, key, VersionSelector.CURRENT);
538: }
539:
540: /**
541: * Select records from the specified table by key using version selector
542: * @param table class corresponding to the table
543: * @param field indexed field
544: * @param key key value
545: * @param selector version selector
546: * @return iterator through selected records. This iterator doesn't support remove() method.
547: * If there are no instances of such class in the database, then empty iterator is returned
548: * @exception NoSuchIndexException if there is no index for the specified field
549: */
550: public <T extends CVersion> IterableIterator<T> find(Class table,
551: String field, Key key, VersionSelector selector)
552: throws NoSuchIndexException {
553: TableDescriptor desc = lookupTable(table);
554: if (desc == null) {
555: return new EmptyIterator<T>();
556: }
557: TableDescriptor.IndexDescriptor idesc = desc.findIndex(field);
558: if (idesc == null) {
559: throw new NoSuchIndexException(field);
560: }
561: key = idesc.checkKey(key);
562: return new IndexFilter<T>(idesc.index, root, selector)
563: .iterator(key, key, GenericIndex.ASCENT_ORDER);
564: }
565:
566: /**
567: * Select records from the specified table by key range using version selector
568: * @param table class corresponding to the table
569: * @param field indexed field
570: * @param min minimal key value
571: * @param max maximal key value
572: * @param selector version selector
573: * @param order key sort order: GenericIndex.ASCENT_ORDER or GenericIndex.DESCENT_ORDER
574: * @return iterator through selected records. This iterator doesn't support remove() method.
575: * If there are no instances of such class in the database, then empty iterator is returned
576: * @exception NoSuchIndexException if there is no index for the specified field
577: */
578: public <T extends CVersion> IterableIterator<T> find(Class table,
579: String field, Key min, Key max, VersionSelector selector,
580: int order) {
581: TableDescriptor desc = lookupTable(table);
582: if (desc == null) {
583: return new EmptyIterator<T>();
584: }
585: TableDescriptor.IndexDescriptor idesc = desc.findIndex(field);
586: if (idesc == null) {
587: throw new NoSuchIndexException(field);
588: }
589: return new IndexFilter<T>(idesc.index, root, selector)
590: .iterator(idesc.checkKey(min), idesc.checkKey(max),
591: order);
592: }
593:
594: /**
595: * Perform full text search through the current version of all objects in the database
596: * @param query full text search search query. It should be compliant with Lucene query syntax and may
597: * refer to the particular fields or to any full text searchable field:
598: * <ul>
599: * <li>"name:John" - select document with field "name" containing word "John"</li>
600: * <li>"title:magic AND author:Clark" - select document with field "title" containing word "magic" and field "author"
601: * containing word "Clark"</li>
602: * <li>"atomic nuclear power" - select any document which full text searchable fields contain any of the specified words</li>
603: * <li>"Class:com.eshop.Player AND description:DivX" - select objects with class com.eshop.Player which description contains word "DivX"</li>
604: * </ul>
605: * @param limit search result limit
606: * @return array with matched documents
607: * @exception FullTextSearchQuerySyntaxError if query syntax is invalid
608: */
609: public FullTextSearchResult[] fullTextSearch(String query, int limit) {
610: return fullTextSearch(query, limit, VersionSelector.CURRENT,
611: VersionSortOrder.NONE);
612: }
613:
614: /**
615: * Perform full text search using version selector
616: * @param query full text search search query. It should be compliant with Lucene query syntax and may
617: * refer to the particular fields or to any full text searchable field:
618: * <ul>
619: * <li>"name:John" - select document with field "name" containing word "John"</li>
620: * <li>"title:magic AND author:Clark" - select document with field "title" containing word "magic" and field "author"
621: * containing word "Clark"</li>
622: * <li>"atomic nuclear power" - select any document which full text searchable fields contain any of the specified words</li>
623: * <li>"Class:com.eshop.Player AND description:DivX" - select objects with class com.eshop.Player which description contains word "DivX"</li>
624: * </ul>
625: * @param limit search result limit
626: * @param selector version selector
627: * @param order result set versions sort order (if VersionSortOrder.NONE then order of documents returned by Lucene
628: * is preserved - documents are sorted by score)
629: * @return array with matched documents
630: * @exception FullTextSearchQuerySyntaxError if query syntax is invalid
631: */
632: public FullTextSearchResult[] fullTextSearch(String query,
633: int limit, VersionSelector selector, VersionSortOrder order) {
634: ArrayList<FullTextSearchResult> result = null;
635: root.sharedLock();
636: try {
637: Hits hits;
638: try {
639: IndexSearcher searcher = new IndexSearcher(
640: getIndexReader());
641: if (selector.kind == VersionSelector.Kind.TimeSlice) {
642: long from = selector.from != null ? selector.from
643: .getTime() : 0;
644: long till = selector.till != null ? selector.till
645: .getTime() : System.currentTimeMillis();
646: StringBuilder sb = new StringBuilder("Created:[");
647: sb.append(DateTools.timeToString(from,
648: DateTools.Resolution.MINUTE));
649: sb.append(" TO ");
650: sb.append(DateTools.timeToString(till,
651: DateTools.Resolution.MINUTE));
652: sb.append("] AND (");
653: sb.append(query);
654: sb.append(')');
655: query = sb.toString();
656: }
657:
658: QueryParser parser = new QueryParser("Any", analyzer);
659: hits = searcher.search(parser.parse(query));
660: } catch (IOException x) {
661: throw new IOError(x);
662: } catch (ParseException x) {
663: throw new FullTextSearchQuerySyntaxError(x);
664: }
665: result = new ArrayList<FullTextSearchResult>();
666: Iterator<FullTextSearchResult> iterator = new FullTextSearchIterator(
667: hits, storage, selector);
668: while (--limit >= 0 && iterator.hasNext()) {
669: result.add(iterator.next());
670: }
671: } finally {
672: root.unlock();
673: }
674: FullTextSearchResult[] arr = result
675: .toArray(new FullTextSearchResult[result.size()]);
676: if (order != VersionSortOrder.NONE) {
677: final int delta = order.delta;
678: Arrays.sort(arr, new Comparator<FullTextSearchResult>() {
679: public int compare(FullTextSearchResult r1,
680: FullTextSearchResult r2) {
681: long t1 = r1.getVersion().transId;
682: long t2 = r2.getVersion().transId;
683: return t1 < t2 ? -delta : t1 == t2 ? 0 : delta;
684: }
685: });
686: }
687: return arr;
688: }
689:
690: /**
691: * Restore full text search index.
692: * 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
693: * corrupted in case of failure. In this case index can be reconstructed using this method.
694: */
695: public synchronized void restoreFullTextIndex() {
696: root.exclusiveLock();
697: try {
698: try {
699: if (indexWriter != null) {
700: indexWriter.close();
701: }
702: if (indexReader != null) {
703: indexReader.close();
704: indexReader = null;
705: }
706: indexWriter = new IndexWriter(dir, analyzer, true);
707: for (TableDescriptor table : root.tables) {
708: for (CVersionHistory<?> vh : table) {
709: for (CVersion v : vh) {
710: if (!v.isDeletedVersion()) {
711: Document doc = table.buildDocument(v);
712: if (doc != null) {
713: indexWriter.addDocument(doc);
714: }
715: }
716: }
717: }
718: }
719: indexWriter.optimize();
720: } catch (IOException x) {
721: throw new IOError(x);
722: }
723: } finally {
724: root.unlock();
725: }
726: }
727:
728: /**
729: * Optimize full text index. This method can be periodically called by application (preferably in idle time)
730: * to optimize full text search index
731: */
732: public synchronized void optimizeFullTextIndex() {
733: root.exclusiveLock();
734: try {
735: try {
736: getIndexWriter().optimize();
737: } catch (IOException x) {
738: throw new IOError(x);
739: }
740: } finally {
741: root.unlock();
742: }
743: }
744:
745: /**
746: * Get storage associated with this database
747: * @return underlying storage
748: */
749: public Storage getStorage() {
750: return storage;
751: }
752:
753: /**
754: * Get resource used to synchronize access to the database
755: */
756: public IResource getResource() {
757: return root;
758: }
759:
760: /**
761: * Get user data. Continuous uses Perst root objects for its own purposes.
762: * But this methods allows application to specify its own root and store in the Perst
763: * storage non-versioned objects.
764: * @return reference to the persistent object previously stored by setUserData()
765: */
766: public IPersistent getUserData() {
767: root.sharedLock();
768: try {
769: return root.userData;
770: } finally {
771: root.unlock();
772: }
773: }
774:
775: /**
776: * Set user data. Continuous uses Perst root objects for its own purposes.
777: * But this methods allows application to specify its own root and store in the Perst
778: * storage non-versioned objects.
779: * @param obj reference to the persistent object which will be stored in Perst root object
780: */
781: public void setUserData(IPersistent obj) {
782: root.exclusiveLock();
783: try {
784: root.userData = obj;
785: root.modify();
786: } finally {
787: root.unlock();
788: }
789: }
790:
791: /**
792: * Static instance of the database which can be used by application working with the single database
793: */
794: public static CDatabase instance = new CDatabase();
795:
796: static TransactionContext getTransactionContext() {
797: return transactionContext.get();
798: }
799:
800: static TransactionContext getWriteTransactionContext() {
801: TransactionContext ctx = transactionContext.get();
802: if (ctx == null) {
803: throw new TransactionNotStartedException();
804: }
805: return ctx;
806: }
807:
808: void openFullTextIndex(RootObject root, String path) {
809: boolean create;
810: if (path == null) {
811: // Store Lucene index in Perst database
812: create = root.catalogue == null;
813: dir = new PerstDirectory(root);
814: } else {
815: File file = new File(path);
816: create = !file.exists();
817: try {
818: dir = FSDirectory.getDirectory(path);
819: } catch (IOException x) {
820: throw new IOError(x);
821: }
822: }
823: analyzer = new StandardAnalyzer();
824: try {
825: indexWriter = new IndexWriter(dir, analyzer, create);
826: } catch (IOException x) {
827: throw new IOError(x);
828: }
829: }
830:
831: void excludeFromFullTextIndex(TableDescriptor desc, CVersion v) {
832: if (desc.isFullTextSearchable()) {
833: Term term = new Term("Oid", Integer.toString(v.getOid()));
834: try {
835: getIndexReader().deleteDocuments(term);
836: } catch (IOException x) {
837: throw new IOError(x);
838: }
839: }
840: }
841:
842: void includeInFullTextIndex(Document doc) {
843: if (doc != null) {
844: try {
845: getIndexWriter().addDocument(doc);
846: } catch (IOException x) {
847: throw new IOError(x);
848: }
849: }
850: }
851:
852: synchronized TableDescriptor lookupTable(Class type) {
853: return typeMap.get(type);
854: }
855:
856: synchronized TableDescriptor getTable(Class type) {
857: TableDescriptor desc = typeMap.get(type);
858: if (desc == null) {
859: desc = new TableDescriptor(storage, type);
860: typeMap.put(type, desc);
861: root.tables.add(desc);
862: }
863: return desc;
864: }
865:
866: synchronized IndexWriter getIndexWriter() throws IOException {
867: if (indexWriter == null) {
868: if (indexReader != null) {
869: indexReader.close();
870: indexReader = null;
871: }
872: indexWriter = new IndexWriter(dir, analyzer, false);
873: }
874: return indexWriter;
875: }
876:
877: synchronized IndexReader getIndexReader() throws IOException {
878: if (indexReader == null) {
879: if (indexWriter != null) {
880: indexWriter.close();
881: indexWriter = null;
882: }
883: indexReader = IndexReader.open(dir);
884: }
885: return indexReader;
886: }
887:
888: static ThreadLocal<TransactionContext> transactionContext = new ThreadLocal<TransactionContext>();
889:
890: Storage storage;
891: RootObject root;
892: HashMap<Class, TableDescriptor> typeMap;
893: IndexWriter indexWriter;
894: IndexReader indexReader;
895: StandardAnalyzer analyzer;
896: Directory dir;
897: long minTransSeqNo;
898: long maxTransSeqNo;
899: long lastActiveTransId;
900: }
|