EMMA Coverage Report (generated Sun Mar 01 22:06:14 CET 2015)
[all classes][org.h2.fulltext]

COVERAGE SUMMARY FOR SOURCE FILE [FullTextLucene.java]

nameclass, %method, %block, %line, %
FullTextLucene.java100% (3/3)89%  (25/28)87%  (1087/1253)86%  (235.8/274)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class FullTextLucene100% (1/1)83%  (15/18)83%  (627/755)82%  (136.4/166)
FullTextLucene (): void 0%   (0/1)0%   (0/3)0%   (0/2)
convertException (Exception): SQLException 0%   (0/1)0%   (0/12)0%   (0/3)
searchData (Connection, String, int, int): ResultSet 0%   (0/1)0%   (0/7)0%   (0/1)
search (Connection, String, int, int, boolean): ResultSet 100% (1/1)60%  (117/194)63%  (24/38)
removeIndexAccess (FullTextLucene$IndexAccess, String): void 100% (1/1)71%  (22/31)74%  (7.4/10)
getIndexPath (Connection): String 100% (1/1)86%  (37/43)91%  (10/11)
getIndexAccess (Connection): FullTextLucene$IndexAccess 100% (1/1)90%  (77/86)84%  (16/19)
dropIndex (Connection, String, String): void 100% (1/1)96%  (22/23)89%  (8/9)
init (Connection): void 100% (1/1)96%  (100/104)86%  (12/14)
<static initializer> 100% (1/1)100% (7/7)100% (2/2)
createIndex (Connection, String, String, String): void 100% (1/1)100% (30/30)100% (9/9)
createOrDropTrigger (Connection, String, String, boolean): void 100% (1/1)100% (68/68)100% (8/8)
createTrigger (Connection, String, String): void 100% (1/1)100% (6/6)100% (2/2)
dropAll (Connection): void 100% (1/1)100% (13/13)100% (5/5)
indexExistingRows (Connection, String, String): void 100% (1/1)100% (65/65)100% (13/13)
reindex (Connection): void 100% (1/1)100% (35/35)100% (12/12)
removeIndexFiles (Connection): void 100% (1/1)100% (21/21)100% (7/7)
search (Connection, String, int, int): ResultSet 100% (1/1)100% (7/7)100% (1/1)
     
class FullTextLucene$FullTextTrigger100% (1/1)100% (9/9)92%  (457/495)92%  (98.9/108)
delete (Object [], boolean): void 100% (1/1)84%  (21/25)78%  (7/9)
insert (Object [], boolean): void 100% (1/1)88%  (110/125)87%  (19.9/23)
commitIndex (): void 100% (1/1)88%  (30/34)80%  (8/10)
getQuery (Object []): String 100% (1/1)93%  (71/76)92%  (11/12)
init (Connection, String, String, String, boolean, int): void 100% (1/1)95%  (178/188)97%  (38/39)
FullTextLucene$FullTextTrigger (): void 100% (1/1)100% (3/3)100% (1/1)
close (): void 100% (1/1)100% (12/12)100% (4/4)
fire (Connection, Object [], Object []): void 100% (1/1)100% (31/31)100% (9/9)
remove (): void 100% (1/1)100% (1/1)100% (1/1)
     
class FullTextLucene$IndexAccess100% (1/1)100% (1/1)100% (3/3)100% (1/1)
FullTextLucene$IndexAccess (): void 100% (1/1)100% (3/3)100% (1/1)

1/*
2 * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0,
3 * and the EPL 1.0 (http://h2database.com/html/license.html).
4 * Initial Developer: H2 Group
5 */
6package org.h2.fulltext;
7 
8import java.io.IOException;
9import java.sql.Connection;
10import java.sql.DatabaseMetaData;
11import java.sql.PreparedStatement;
12import java.sql.ResultSet;
13import java.sql.SQLException;
14import java.sql.Statement;
15import java.util.ArrayList;
16import java.util.HashMap;
17import org.apache.lucene.analysis.Analyzer;
18import org.apache.lucene.analysis.standard.StandardAnalyzer;
19import org.apache.lucene.document.DateTools;
20import org.apache.lucene.document.Document;
21import org.apache.lucene.document.Field;
22import org.apache.lucene.index.IndexReader;
23import org.apache.lucene.index.Term;
24import org.apache.lucene.queryParser.QueryParser;
25import org.apache.lucene.search.IndexSearcher;
26import org.apache.lucene.search.Query;
27import org.apache.lucene.search.Searcher;
28import org.h2.api.Trigger;
29import org.h2.command.Parser;
30import org.h2.engine.Session;
31import org.h2.expression.ExpressionColumn;
32import org.h2.jdbc.JdbcConnection;
33import org.h2.store.fs.FileUtils;
34import org.h2.tools.SimpleResultSet;
35import org.h2.util.New;
36import org.h2.util.StatementBuilder;
37import org.h2.util.StringUtils;
38import org.h2.util.Utils;
39import java.io.File;
40import org.apache.lucene.search.ScoreDoc;
41import org.apache.lucene.search.TopDocs;
42import org.apache.lucene.store.FSDirectory;
43import org.apache.lucene.store.Directory;
44import org.apache.lucene.store.RAMDirectory;
45import org.apache.lucene.util.Version;
46import org.apache.lucene.index.IndexWriter;
47 
48/**
49 * This class implements the full text search based on Apache Lucene.
50 * Most methods can be called using SQL statements as well.
51 */
52public class FullTextLucene extends FullText {
53 
54    /**
55     * Whether the text content should be stored in the Lucene index.
56     */
57    protected static final boolean STORE_DOCUMENT_TEXT_IN_INDEX =
58            Utils.getProperty("h2.storeDocumentTextInIndex", false);
59 
60    private static final HashMap<String, IndexAccess> INDEX_ACCESS = New.hashMap();
61    private static final String TRIGGER_PREFIX = "FTL_";
62    private static final String SCHEMA = "FTL";
63    private static final String LUCENE_FIELD_DATA = "_DATA";
64    private static final String LUCENE_FIELD_QUERY = "_QUERY";
65    private static final String LUCENE_FIELD_MODIFIED = "_modified";
66    private static final String LUCENE_FIELD_COLUMN_PREFIX = "_";
67 
68    /**
69     * The prefix for a in-memory path. This prefix is only used internally
70     * within this class and not related to the database URL.
71     */
72    private static final String IN_MEMORY_PREFIX = "mem:";
73 
74    /**
75     * Initializes full text search functionality for this database. This adds
76     * the following Java functions to the database:
77     * <ul>
78     * <li>FTL_CREATE_INDEX(schemaNameString, tableNameString,
79     * columnListString)</li>
80     * <li>FTL_SEARCH(queryString, limitInt, offsetInt): result set</li>
81     * <li>FTL_REINDEX()</li>
82     * <li>FTL_DROP_ALL()</li>
83     * </ul>
84     * It also adds a schema FTL to the database where bookkeeping information
85     * is stored. This function may be called from a Java application, or by
86     * using the SQL statements:
87     *
88     * <pre>
89     * CREATE ALIAS IF NOT EXISTS FTL_INIT FOR
90     *      &quot;org.h2.fulltext.FullTextLucene.init&quot;;
91     * CALL FTL_INIT();
92     * </pre>
93     *
94     * @param conn the connection
95     */
96    public static void init(Connection conn) throws SQLException {
97        Statement stat = conn.createStatement();
98        stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
99        stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA +
100                ".INDEXES(SCHEMA VARCHAR, TABLE VARCHAR, " +
101                "COLUMNS VARCHAR, PRIMARY KEY(SCHEMA, TABLE))");
102        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_CREATE_INDEX FOR \"" +
103                FullTextLucene.class.getName() + ".createIndex\"");
104        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_DROP_INDEX FOR \"" +
105                FullTextLucene.class.getName() + ".dropIndex\"");
106        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH FOR \"" +
107                FullTextLucene.class.getName() + ".search\"");
108        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH_DATA FOR \"" +
109                FullTextLucene.class.getName() + ".searchData\"");
110        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_REINDEX FOR \"" +
111                FullTextLucene.class.getName() + ".reindex\"");
112        stat.execute("CREATE ALIAS IF NOT EXISTS FTL_DROP_ALL FOR \"" +
113                FullTextLucene.class.getName() + ".dropAll\"");
114        try {
115            getIndexAccess(conn);
116        } catch (SQLException e) {
117            throw convertException(e);
118        }
119    }
120 
121    /**
122     * Create a new full text index for a table and column list. Each table may
123     * only have one index at any time.
124     *
125     * @param conn the connection
126     * @param schema the schema name of the table (case sensitive)
127     * @param table the table name (case sensitive)
128     * @param columnList the column list (null for all columns)
129     */
130    public static void createIndex(Connection conn, String schema,
131            String table, String columnList) throws SQLException {
132        init(conn);
133        PreparedStatement prep = conn.prepareStatement("INSERT INTO " + SCHEMA
134                + ".INDEXES(SCHEMA, TABLE, COLUMNS) VALUES(?, ?, ?)");
135        prep.setString(1, schema);
136        prep.setString(2, table);
137        prep.setString(3, columnList);
138        prep.execute();
139        createTrigger(conn, schema, table);
140        indexExistingRows(conn, schema, table);
141    }
142 
143    /**
144     * Drop an existing full text index for a table. This method returns
145     * silently if no index for this table exists.
146     *
147     * @param conn the connection
148     * @param schema the schema name of the table (case sensitive)
149     * @param table the table name (case sensitive)
150     */
151    public static void dropIndex(Connection conn, String schema, String table)
152            throws SQLException {
153        init(conn);
154 
155        PreparedStatement prep = conn.prepareStatement("DELETE FROM " + SCHEMA
156                + ".INDEXES WHERE SCHEMA=? AND TABLE=?");
157        prep.setString(1, schema);
158        prep.setString(2, table);
159        int rowCount = prep.executeUpdate();
160        if (rowCount == 0) {
161            return;
162        }
163 
164        reindex(conn);
165    }
166 
167    /**
168     * Re-creates the full text index for this database. Calling this method is
169     * usually not needed, as the index is kept up-to-date automatically.
170     *
171     * @param conn the connection
172     */
173    public static void reindex(Connection conn) throws SQLException {
174        init(conn);
175        removeAllTriggers(conn, TRIGGER_PREFIX);
176        removeIndexFiles(conn);
177        Statement stat = conn.createStatement();
178        ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".INDEXES");
179        while (rs.next()) {
180            String schema = rs.getString("SCHEMA");
181            String table = rs.getString("TABLE");
182            createTrigger(conn, schema, table);
183            indexExistingRows(conn, schema, table);
184        }
185    }
186 
187    /**
188     * Drops all full text indexes from the database.
189     *
190     * @param conn the connection
191     */
192    public static void dropAll(Connection conn) throws SQLException {
193        Statement stat = conn.createStatement();
194        stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA);
195        removeAllTriggers(conn, TRIGGER_PREFIX);
196        removeIndexFiles(conn);
197    }
198 
199    /**
200     * Searches from the full text index for this database.
201     * The returned result set has the following column:
202     * <ul><li>QUERY (varchar): the query to use to get the data.
203     * The query does not include 'SELECT * FROM '. Example:
204     * PUBLIC.TEST WHERE ID = 1
205     * </li><li>SCORE (float) the relevance score as returned by Lucene.
206     * </li></ul>
207     *
208     * @param conn the connection
209     * @param text the search query
210     * @param limit the maximum number of rows or 0 for no limit
211     * @param offset the offset or 0 for no offset
212     * @return the result set
213     */
214    public static ResultSet search(Connection conn, String text, int limit,
215            int offset) throws SQLException {
216        return search(conn, text, limit, offset, false);
217    }
218 
219    /**
220     * Searches from the full text index for this database. The result contains
221     * the primary key data as an array. The returned result set has the
222     * following columns:
223     * <ul>
224     * <li>SCHEMA (varchar): the schema name. Example: PUBLIC</li>
225     * <li>TABLE (varchar): the table name. Example: TEST</li>
226     * <li>COLUMNS (array of varchar): comma separated list of quoted column
227     * names. The column names are quoted if necessary. Example: (ID)</li>
228     * <li>KEYS (array of values): comma separated list of values.
229     * Example: (1)</li>
230     * <li>SCORE (float) the relevance score as returned by Lucene.</li>
231     * </ul>
232     *
233     * @param conn the connection
234     * @param text the search query
235     * @param limit the maximum number of rows or 0 for no limit
236     * @param offset the offset or 0 for no offset
237     * @return the result set
238     */
239    public static ResultSet searchData(Connection conn, String text, int limit,
240            int offset) throws SQLException {
241        return search(conn, text, limit, offset, true);
242    }
243 
244    /**
245     * Convert an exception to a fulltext exception.
246     *
247     * @param e the original exception
248     * @return the converted SQL exception
249     */
250    protected static SQLException convertException(Exception e) {
251        SQLException e2 = new SQLException(
252                "Error while indexing document", "FULLTEXT");
253        e2.initCause(e);
254        return e2;
255    }
256 
257    /**
258     * Create the trigger.
259     *
260     * @param conn the database connection
261     * @param schema the schema name
262     * @param table the table name
263     */
264    protected static void createTrigger(Connection conn, String schema,
265            String table) throws SQLException {
266        createOrDropTrigger(conn, schema, table, true);
267    }
268 
269    private static void createOrDropTrigger(Connection conn,
270            String schema, String table, boolean create) throws SQLException {
271        Statement stat = conn.createStatement();
272        String trigger = StringUtils.quoteIdentifier(schema) + "." +
273                StringUtils.quoteIdentifier(TRIGGER_PREFIX + table);
274        stat.execute("DROP TRIGGER IF EXISTS " + trigger);
275        if (create) {
276            StringBuilder buff = new StringBuilder(
277                    "CREATE TRIGGER IF NOT EXISTS ");
278            // the trigger is also called on rollback because transaction
279            // rollback will not undo the changes in the Lucene index
280            buff.append(trigger).
281                append(" AFTER INSERT, UPDATE, DELETE, ROLLBACK ON ").
282                append(StringUtils.quoteIdentifier(schema)).
283                append('.').
284                append(StringUtils.quoteIdentifier(table)).
285                append(" FOR EACH ROW CALL \"").
286                append(FullTextLucene.FullTextTrigger.class.getName()).
287                append('\"');
288            stat.execute(buff.toString());
289        }
290    }
291 
292    /**
293     * Get the index writer/searcher wrapper for the given connection.
294     *
295     * @param conn the connection
296     * @return the index access wrapper
297     */
298    protected static IndexAccess getIndexAccess(Connection conn)
299            throws SQLException {
300        String path = getIndexPath(conn);
301        synchronized (INDEX_ACCESS) {
302            IndexAccess access = INDEX_ACCESS.get(path);
303            if (access == null) {
304                try {
305                    Directory indexDir = path.startsWith(IN_MEMORY_PREFIX) ?
306                            new RAMDirectory() : FSDirectory.open(new File(path));
307                    boolean recreate = !IndexReader.indexExists(indexDir);
308                    Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
309                    IndexWriter writer = new IndexWriter(indexDir, analyzer,
310                            recreate, IndexWriter.MaxFieldLength.UNLIMITED);
311                    //see http://wiki.apache.org/lucene-java/NearRealtimeSearch
312                    IndexReader reader = writer.getReader();
313                    access = new IndexAccess();
314                    access.writer = writer;
315                    access.reader = reader;
316                    access.searcher = new IndexSearcher(reader);
317                } catch (IOException e) {
318                    throw convertException(e);
319                }
320                INDEX_ACCESS.put(path, access);
321            }
322            return access;
323        }
324    }
325 
326    /**
327     * Get the path of the Lucene index for this database.
328     *
329     * @param conn the database connection
330     * @return the path
331     */
332    protected static String getIndexPath(Connection conn) throws SQLException {
333        Statement stat = conn.createStatement();
334        ResultSet rs = stat.executeQuery("CALL DATABASE_PATH()");
335        rs.next();
336        String path = rs.getString(1);
337        if (path == null) {
338            return IN_MEMORY_PREFIX + conn.getCatalog();
339        }
340        int index = path.lastIndexOf(':');
341        // position 1 means a windows drive letter is used, ignore that
342        if (index > 1) {
343            path = path.substring(index + 1);
344        }
345        rs.close();
346        return path;
347    }
348 
349    /**
350     * Add the existing data to the index.
351     *
352     * @param conn the database connection
353     * @param schema the schema name
354     * @param table the table name
355     */
356    protected static void indexExistingRows(Connection conn, String schema,
357            String table) throws SQLException {
358        FullTextLucene.FullTextTrigger existing = new FullTextLucene.FullTextTrigger();
359        existing.init(conn, schema, null, table, false, Trigger.INSERT);
360        String sql = "SELECT * FROM " + StringUtils.quoteIdentifier(schema) +
361                "." + StringUtils.quoteIdentifier(table);
362        ResultSet rs = conn.createStatement().executeQuery(sql);
363        int columnCount = rs.getMetaData().getColumnCount();
364        while (rs.next()) {
365            Object[] row = new Object[columnCount];
366            for (int i = 0; i < columnCount; i++) {
367                row[i] = rs.getObject(i + 1);
368            }
369            existing.insert(row, false);
370        }
371        existing.commitIndex();
372    }
373 
374    private static void removeIndexFiles(Connection conn) throws SQLException {
375        String path = getIndexPath(conn);
376        IndexAccess access = INDEX_ACCESS.get(path);
377        if (access != null) {
378            removeIndexAccess(access, path);
379        }
380        if (!path.startsWith(IN_MEMORY_PREFIX)) {
381            FileUtils.deleteRecursive(path, false);
382        }
383    }
384 
385    /**
386     * Close the index writer and searcher and remove them from the index access
387     * set.
388     *
389     * @param access the index writer/searcher wrapper
390     * @param indexPath the index path
391     */
392    protected static void removeIndexAccess(IndexAccess access, String indexPath)
393            throws SQLException {
394        synchronized (INDEX_ACCESS) {
395            try {
396                INDEX_ACCESS.remove(indexPath);
397                access.searcher.close();
398                access.reader.close();
399                access.writer.close();
400            } catch (Exception e) {
401                throw convertException(e);
402            }
403        }
404    }
405 
406    /**
407     * Do the search.
408     *
409     * @param conn the database connection
410     * @param text the query
411     * @param limit the limit
412     * @param offset the offset
413     * @param data whether the raw data should be returned
414     * @return the result set
415     */
416    protected static ResultSet search(Connection conn, String text,
417            int limit, int offset, boolean data) throws SQLException {
418        SimpleResultSet result = createResultSet(data);
419        if (conn.getMetaData().getURL().startsWith("jdbc:columnlist:")) {
420            // this is just to query the result set columns
421            return result;
422        }
423        if (text == null || text.trim().length() == 0) {
424            return result;
425        }
426        try {
427            IndexAccess access = getIndexAccess(conn);
428            // take a reference as the searcher may change
429            Searcher searcher = access.searcher;
430            // reuse the same analyzer; it's thread-safe;
431            // also allows subclasses to control the analyzer used.
432            Analyzer analyzer = access.writer.getAnalyzer();
433            QueryParser parser = new QueryParser(Version.LUCENE_30,
434                    LUCENE_FIELD_DATA, analyzer);
435            Query query = parser.parse(text);
436            // Lucene 3 insists on a hard limit and will not provide
437            // a total hits value. Take at least 100 which is
438            // an optimal limit for Lucene as any more
439            // will trigger writing results to disk.
440            int maxResults = (limit == 0 ? 100 : limit) + offset;
441            TopDocs docs = searcher.search(query, maxResults);
442            if (limit == 0) {
443                limit = docs.totalHits;
444            }
445            for (int i = 0, len = docs.scoreDocs.length;
446                    i < limit && i + offset < docs.totalHits
447                    && i + offset < len; i++) {
448                ScoreDoc sd = docs.scoreDocs[i + offset];
449                Document doc = searcher.doc(sd.doc);
450                float score = sd.score;
451                String q = doc.get(LUCENE_FIELD_QUERY);
452                if (data) {
453                    int idx = q.indexOf(" WHERE ");
454                    JdbcConnection c = (JdbcConnection) conn;
455                    Session session = (Session) c.getSession();
456                    Parser p = new Parser(session);
457                    String tab = q.substring(0, idx);
458                    ExpressionColumn expr = (ExpressionColumn) p.parseExpression(tab);
459                    String schemaName = expr.getOriginalTableAliasName();
460                    String tableName = expr.getColumnName();
461                    q = q.substring(idx + " WHERE ".length());
462                    Object[][] columnData = parseKey(conn, q);
463                    result.addRow(
464                            schemaName,
465                            tableName,
466                            columnData[0],
467                            columnData[1],
468                            score);
469                } else {
470                    result.addRow(q, score);
471                }
472            }
473        } catch (Exception e) {
474            throw convertException(e);
475        }
476        return result;
477    }
478 
479    /**
480     * Trigger updates the index when a inserting, updating, or deleting a row.
481     */
482    public static class FullTextTrigger implements Trigger {
483 
484        protected String schema;
485        protected String table;
486        protected int[] keys;
487        protected int[] indexColumns;
488        protected String[] columns;
489        protected int[] columnTypes;
490        protected String indexPath;
491        protected IndexAccess indexAccess;
492 
493        /**
494         * INTERNAL
495         */
496        @Override
497        public void init(Connection conn, String schemaName, String triggerName,
498                String tableName, boolean before, int type) throws SQLException {
499            this.schema = schemaName;
500            this.table = tableName;
501            this.indexPath = getIndexPath(conn);
502            this.indexAccess = getIndexAccess(conn);
503            ArrayList<String> keyList = New.arrayList();
504            DatabaseMetaData meta = conn.getMetaData();
505            ResultSet rs = meta.getColumns(null,
506                    StringUtils.escapeMetaDataPattern(schemaName),
507                    StringUtils.escapeMetaDataPattern(tableName),
508                    null);
509            ArrayList<String> columnList = New.arrayList();
510            while (rs.next()) {
511                columnList.add(rs.getString("COLUMN_NAME"));
512            }
513            columnTypes = new int[columnList.size()];
514            columns = new String[columnList.size()];
515            columnList.toArray(columns);
516            rs = meta.getColumns(null,
517                    StringUtils.escapeMetaDataPattern(schemaName),
518                    StringUtils.escapeMetaDataPattern(tableName),
519                    null);
520            for (int i = 0; rs.next(); i++) {
521                columnTypes[i] = rs.getInt("DATA_TYPE");
522            }
523            if (keyList.size() == 0) {
524                rs = meta.getPrimaryKeys(null,
525                        StringUtils.escapeMetaDataPattern(schemaName),
526                        tableName);
527                while (rs.next()) {
528                    keyList.add(rs.getString("COLUMN_NAME"));
529                }
530            }
531            if (keyList.size() == 0) {
532                throw throwException("No primary key for table " + tableName);
533            }
534            ArrayList<String> indexList = New.arrayList();
535            PreparedStatement prep = conn.prepareStatement(
536                    "SELECT COLUMNS FROM " + SCHEMA
537                    + ".INDEXES WHERE SCHEMA=? AND TABLE=?");
538            prep.setString(1, schemaName);
539            prep.setString(2, tableName);
540            rs = prep.executeQuery();
541            if (rs.next()) {
542                String cols = rs.getString(1);
543                if (cols != null) {
544                    for (String s : StringUtils.arraySplit(cols, ',', true)) {
545                        indexList.add(s);
546                    }
547                }
548            }
549            if (indexList.size() == 0) {
550                indexList.addAll(columnList);
551            }
552            keys = new int[keyList.size()];
553            setColumns(keys, keyList, columnList);
554            indexColumns = new int[indexList.size()];
555            setColumns(indexColumns, indexList, columnList);
556        }
557 
558        /**
559         * INTERNAL
560         */
561        @Override
562        public void fire(Connection conn, Object[] oldRow, Object[] newRow)
563                throws SQLException {
564            if (oldRow != null) {
565                if (newRow != null) {
566                    // update
567                    if (hasChanged(oldRow, newRow, indexColumns)) {
568                        delete(oldRow, false);
569                        insert(newRow, true);
570                    }
571                } else {
572                    // delete
573                    delete(oldRow, true);
574                }
575            } else if (newRow != null) {
576                // insert
577                insert(newRow, true);
578            }
579        }
580 
581        /**
582         * INTERNAL
583         */
584        @Override
585        public void close() throws SQLException {
586            if (indexAccess != null) {
587                removeIndexAccess(indexAccess, indexPath);
588                indexAccess = null;
589            }
590        }
591 
592        /**
593         * INTERNAL
594         */
595        @Override
596        public void remove() {
597            // ignore
598        }
599 
600        /**
601         * Commit all changes to the Lucene index.
602         */
603        void commitIndex() throws SQLException {
604            try {
605                indexAccess.writer.commit();
606                // recreate Searcher with the IndexWriter's reader.
607                indexAccess.searcher.close();
608                indexAccess.reader.close();
609                IndexReader reader = indexAccess.writer.getReader();
610                indexAccess.reader = reader;
611                indexAccess.searcher = new IndexSearcher(reader);
612            } catch (IOException e) {
613                throw convertException(e);
614            }
615        }
616 
617        /**
618         * Add a row to the index.
619         *
620         * @param row the row
621         * @param commitIndex whether to commit the changes to the Lucene index
622         */
623        protected void insert(Object[] row, boolean commitIndex) throws SQLException {
624            String query = getQuery(row);
625            Document doc = new Document();
626            doc.add(new Field(LUCENE_FIELD_QUERY, query,
627                    Field.Store.YES, Field.Index.NOT_ANALYZED));
628            long time = System.currentTimeMillis();
629            doc.add(new Field(LUCENE_FIELD_MODIFIED,
630                    DateTools.timeToString(time, DateTools.Resolution.SECOND),
631                    Field.Store.YES, Field.Index.NOT_ANALYZED));
632            StatementBuilder buff = new StatementBuilder();
633            for (int index : indexColumns) {
634                String columnName = columns[index];
635                String data = asString(row[index], columnTypes[index]);
636                // column names that start with _
637                // must be escaped to avoid conflicts
638                // with internal field names (_DATA, _QUERY, _modified)
639                if (columnName.startsWith(LUCENE_FIELD_COLUMN_PREFIX)) {
640                    columnName = LUCENE_FIELD_COLUMN_PREFIX + columnName;
641                }
642                doc.add(new Field(columnName, data,
643                        Field.Store.NO, Field.Index.ANALYZED));
644                buff.appendExceptFirst(" ");
645                buff.append(data);
646            }
647            Field.Store storeText = STORE_DOCUMENT_TEXT_IN_INDEX ?
648                    Field.Store.YES : Field.Store.NO;
649            doc.add(new Field(LUCENE_FIELD_DATA, buff.toString(), storeText,
650                    Field.Index.ANALYZED));
651            try {
652                indexAccess.writer.addDocument(doc);
653                if (commitIndex) {
654                    commitIndex();
655                }
656            } catch (IOException e) {
657                throw convertException(e);
658            }
659        }
660 
661        /**
662         * Delete a row from the index.
663         *
664         * @param row the row
665         * @param commitIndex whether to commit the changes to the Lucene index
666         */
667        protected void delete(Object[] row, boolean commitIndex) throws SQLException {
668            String query = getQuery(row);
669            try {
670                Term term = new Term(LUCENE_FIELD_QUERY, query);
671                indexAccess.writer.deleteDocuments(term);
672                if (commitIndex) {
673                    commitIndex();
674                }
675            } catch (IOException e) {
676                throw convertException(e);
677            }
678        }
679 
680        private String getQuery(Object[] row) throws SQLException {
681            StatementBuilder buff = new StatementBuilder();
682            if (schema != null) {
683                buff.append(StringUtils.quoteIdentifier(schema)).append('.');
684            }
685            buff.append(StringUtils.quoteIdentifier(table)).append(" WHERE ");
686            for (int columnIndex : keys) {
687                buff.appendExceptFirst(" AND ");
688                buff.append(StringUtils.quoteIdentifier(columns[columnIndex]));
689                Object o = row[columnIndex];
690                if (o == null) {
691                    buff.append(" IS NULL");
692                } else {
693                    buff.append('=').append(FullText.quoteSQL(o, columnTypes[columnIndex]));
694                }
695            }
696            return buff.toString();
697        }
698    }
699 
700    /**
701     * A wrapper for the Lucene writer and searcher.
702     */
703    static class IndexAccess {
704 
705        /**
706         * The index writer.
707         */
708        IndexWriter writer;
709 
710        /**
711         * The index reader.
712         */
713        IndexReader reader;
714 
715        /**
716         * The index searcher.
717         */
718        Searcher searcher;
719    }
720 
721}

[all classes][org.h2.fulltext]
EMMA 2.0.5312 (C) Vladimir Roubtsov