001: package org.apache.lucene.swing.models;
002:
003: /**
004: * Copyright 2005 The Apache Software Foundation
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: */
018:
019: import org.apache.lucene.analysis.Analyzer;
020: import org.apache.lucene.analysis.WhitespaceAnalyzer;
021: import org.apache.lucene.document.Document;
022: import org.apache.lucene.document.Field;
023: import org.apache.lucene.document.Fieldable;
024: import org.apache.lucene.index.IndexWriter;
025: import org.apache.lucene.queryParser.MultiFieldQueryParser;
026: import org.apache.lucene.search.Hits;
027: import org.apache.lucene.search.IndexSearcher;
028: import org.apache.lucene.search.Query;
029: import org.apache.lucene.store.RAMDirectory;
030:
031: import javax.swing.event.TableModelEvent;
032: import javax.swing.event.TableModelListener;
033: import javax.swing.table.AbstractTableModel;
034: import javax.swing.table.TableModel;
035: import java.util.ArrayList;
036:
037: /**
038: * This is a TableModel that encapsulates Lucene
039: * search logic within a TableModel implementation.
040: * It is implemented as a TableModel decorator,
041: * similar to the TableSorter demo from Sun that decorates
042: * a TableModel and provides sorting functionality. The benefit
043: * of this architecture is that you can decorate any TableModel
044: * implementation with this searching table model -- making it
045: * easy to add searching functionaliy to existing JTables -- or
046: * making new search capable table lucene.
047: *
048: * <p>This decorator works by holding a reference to a decorated ot inner
049: * TableModel. All data is stored within that table model, not this
050: * table model. Rather, this table model simply manages links to
051: * data in the inner table model according to the search. All methods on
052: * TableSearcher forward to the inner table model with subtle filtering
053: * or alteration according to the search criteria.
054: *
055: * <p>Using the table model:
056: *
057: * Pass the TableModel you want to decorate in at the constructor. When
058: * the TableModel initializes, it displays all search results. Call
059: * the search method with any valid Lucene search String and the data
060: * will be filtered by the search string. Users can always clear the search
061: * at any time by searching with an empty string. Additionally, you can
062: * add a button calling the clearSearch() method.
063: *
064: * @author Jonathan Simon - jonathan_s_simon@yahoo.com
065: */
066: public class TableSearcher extends AbstractTableModel {
067:
068: /**
069: * The inner table model we are decorating
070: */
071: protected TableModel tableModel;
072:
073: /**
074: * This listener is used to register this class as a listener to
075: * the decorated table model for update events
076: */
077: private TableModelListener tableModelListener;
078:
079: /**
080: * these keeps reference to the decorated table model for data
081: * only rows that match the search criteria are linked
082: */
083: private ArrayList rowToModelIndex = new ArrayList();
084:
085: //Lucene stuff.
086:
087: /**
088: * In memory lucene index
089: */
090: private RAMDirectory directory;
091:
092: /**
093: * Cached lucene analyzer
094: */
095: private Analyzer analyzer;
096:
097: /**
098: * Links between this table model and the decorated table model
099: * are maintained through links based on row number. This is a
100: * key constant to denote "row number" for indexing
101: */
102: private static final String ROW_NUMBER = "ROW_NUMBER";
103:
104: /**
105: * Cache the current search String. Also used internally to
106: * key whether there is an active search running or not. i.e. if
107: * searchString is null, there is no active search.
108: */
109: private String searchString = null;
110:
111: /**
112: * @param tableModel The table model to decorate
113: */
114: public TableSearcher(TableModel tableModel) {
115: analyzer = new WhitespaceAnalyzer();
116: tableModelListener = new TableModelHandler();
117: setTableModel(tableModel);
118: tableModel.addTableModelListener(tableModelListener);
119: clearSearchingState();
120: }
121:
122: /**
123: *
124: * @return The inner table model this table model is decorating
125: */
126: public TableModel getTableModel() {
127: return tableModel;
128: }
129:
130: /**
131: * Set the table model used by this table model
132: * @param tableModel The new table model to decorate
133: */
134: public void setTableModel(TableModel tableModel) {
135:
136: //remove listeners if there...
137: if (this .tableModel != null) {
138: this .tableModel
139: .removeTableModelListener(tableModelListener);
140: }
141:
142: this .tableModel = tableModel;
143: if (this .tableModel != null) {
144: this .tableModel.addTableModelListener(tableModelListener);
145: }
146:
147: //recalculate the links between this table model and
148: //the inner table model since the decorated model just changed
149: reindex();
150:
151: // let all listeners know the table has changed
152: fireTableStructureChanged();
153: }
154:
155: /**
156: * Reset the search results and links to the decorated (inner) table
157: * model from this table model.
158: */
159: private void reindex() {
160: try {
161: // recreate the RAMDirectory
162: directory = new RAMDirectory();
163: IndexWriter writer = new IndexWriter(directory, analyzer,
164: true);
165:
166: // iterate through all rows
167: for (int row = 0; row < tableModel.getRowCount(); row++) {
168:
169: //for each row make a new document
170: Document document = new Document();
171: //add the row number of this row in the decorated table model
172: //this will allow us to retrive the results later
173: //and map this table model's row to a row in the decorated
174: //table model
175: document.add(new Field(ROW_NUMBER, "" + row,
176: Field.Store.YES, Field.Index.TOKENIZED));
177: //iterate through all columns
178: //index the value keyed by the column name
179: //NOTE: there could be a problem with using column names with spaces
180: for (int column = 0; column < tableModel
181: .getColumnCount(); column++) {
182: String columnName = tableModel
183: .getColumnName(column);
184: String columnValue = String.valueOf(
185: tableModel.getValueAt(row, column))
186: .toLowerCase();
187: document.add(new Field(columnName, columnValue,
188: Field.Store.YES, Field.Index.TOKENIZED));
189: }
190: writer.addDocument(document);
191: }
192: writer.optimize();
193: writer.close();
194: } catch (Exception e) {
195: e.printStackTrace();
196: }
197: }
198:
199: /**
200: * @return The current lucene analyzer
201: */
202: public Analyzer getAnalyzer() {
203: return analyzer;
204: }
205:
206: /**
207: * @param analyzer The new analyzer to use
208: */
209: public void setAnalyzer(Analyzer analyzer) {
210: this .analyzer = analyzer;
211: //reindex from the model with the new analyzer
212: reindex();
213:
214: //rerun the search if there is an active search
215: if (isSearching()) {
216: search(searchString);
217: }
218: }
219:
220: /**
221: * Run a new search.
222: *
223: * @param searchString Any valid lucene search string
224: */
225: public void search(String searchString) {
226:
227: //if search string is null or empty, clear the search == search all
228: if (searchString == null || searchString.equals("")) {
229: clearSearchingState();
230: fireTableDataChanged();
231: return;
232: }
233:
234: try {
235: //cache search String
236: this .searchString = searchString;
237:
238: //make a new index searcher with the in memory (RAM) index.
239: IndexSearcher is = new IndexSearcher(directory);
240:
241: //make an array of fields - one for each column
242: String[] fields = new String[tableModel.getColumnCount()];
243: for (int t = 0; t < tableModel.getColumnCount(); t++) {
244: fields[t] = tableModel.getColumnName(t);
245: }
246:
247: //build a query based on the fields, searchString and cached analyzer
248: //NOTE: This is an area for improvement since the MultiFieldQueryParser
249: // has some weirdness.
250: MultiFieldQueryParser parser = new MultiFieldQueryParser(
251: fields, analyzer);
252: Query query = parser.parse(searchString);
253: //run the search
254: Hits hits = is.search(query);
255: //reset this table model with the new results
256: resetSearchResults(hits);
257: } catch (Exception e) {
258: e.printStackTrace();
259: }
260:
261: //notify all listeners that the table has been changed
262: fireTableStructureChanged();
263: }
264:
265: /**
266: *
267: * @param hits The new result set to set this table to.
268: */
269: private void resetSearchResults(Hits hits) {
270: try {
271: //clear our index mapping this table model rows to
272: //the decorated inner table model
273: rowToModelIndex.clear();
274: //iterate through the hits
275: //get the row number stored at the index
276: //that number is the row number of the decorated
277: //tabble model row that we are mapping to
278: for (int t = 0; t < hits.length(); t++) {
279: Document document = hits.doc(t);
280: Fieldable field = document.getField(ROW_NUMBER);
281: rowToModelIndex.add(new Integer(field.stringValue()));
282: }
283: } catch (Exception e) {
284: e.printStackTrace();
285: }
286: }
287:
288: private int getModelRow(int row) {
289: return ((Integer) rowToModelIndex.get(row)).intValue();
290: }
291:
292: /**
293: * Clear the currently active search
294: * Resets the complete dataset of the decorated
295: * table model.
296: */
297: private void clearSearchingState() {
298: searchString = null;
299: rowToModelIndex.clear();
300: for (int t = 0; t < tableModel.getRowCount(); t++) {
301: rowToModelIndex.add(new Integer(t));
302: }
303: }
304:
305: // TableModel interface methods
306: public int getRowCount() {
307: return (tableModel == null) ? 0 : rowToModelIndex.size();
308: }
309:
310: public int getColumnCount() {
311: return (tableModel == null) ? 0 : tableModel.getColumnCount();
312: }
313:
314: public String getColumnName(int column) {
315: return tableModel.getColumnName(column);
316: }
317:
318: public Class getColumnClass(int column) {
319: return tableModel.getColumnClass(column);
320: }
321:
322: public boolean isCellEditable(int row, int column) {
323: return tableModel.isCellEditable(getModelRow(row), column);
324: }
325:
326: public Object getValueAt(int row, int column) {
327: return tableModel.getValueAt(getModelRow(row), column);
328: }
329:
330: public void setValueAt(Object aValue, int row, int column) {
331: tableModel.setValueAt(aValue, getModelRow(row), column);
332: }
333:
334: private boolean isSearching() {
335: return searchString != null;
336: }
337:
338: private class TableModelHandler implements TableModelListener {
339: public void tableChanged(TableModelEvent e) {
340: // If we're not searching, just pass the event along.
341: if (!isSearching()) {
342: clearSearchingState();
343: reindex();
344: fireTableChanged(e);
345: return;
346: }
347:
348: // Something has happened to the data that may have invalidated the search.
349: reindex();
350: search(searchString);
351: fireTableDataChanged();
352: return;
353: }
354:
355: }
356:
357: }
|