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.store.RAMDirectory;
020: import org.apache.lucene.analysis.Analyzer;
021: import org.apache.lucene.analysis.WhitespaceAnalyzer;
022: import org.apache.lucene.index.IndexWriter;
023: import org.apache.lucene.document.Document;
024: import org.apache.lucene.document.Field;
025: import org.apache.lucene.document.Fieldable;
026: import org.apache.lucene.search.IndexSearcher;
027: import org.apache.lucene.search.Query;
028: import org.apache.lucene.search.Hits;
029: import org.apache.lucene.queryParser.MultiFieldQueryParser;
030:
031: import javax.swing.*;
032: import javax.swing.event.ListDataListener;
033: import javax.swing.event.ListDataEvent;
034: import java.util.ArrayList;
035:
036: /**
037: * See table searcher explanation.
038: *
039: * @author Jonathan Simon - jonathan_s_simon@yahoo.com
040: */
041: public class ListSearcher extends AbstractListModel {
042: private ListModel listModel;
043:
044: /**
045: * The reference links between the decorated ListModel
046: * and this list model based on search criteria
047: */
048: private ArrayList rowToModelIndex = new ArrayList();
049:
050: /**
051: * In memory lucene index
052: */
053: private RAMDirectory directory;
054:
055: /**
056: * Cached lucene analyzer
057: */
058: private Analyzer analyzer;
059:
060: /**
061: * Links between this list model and the decorated list model
062: * are maintained through links based on row number. This is a
063: * key constant to denote "row number" for indexing
064: */
065: private static final String ROW_NUMBER = "ROW_NUMBER";
066:
067: /**
068: * Since we only have one field, unlike lists with multiple
069: * fields -- we are just using a constant to denote field name.
070: * This is most likely unnecessary and should be removed at
071: * a later date
072: */
073: private static final String FIELD_NAME = "FIELD_NAME";
074:
075: /**
076: * Cache the current search String. Also used internally to
077: * key whether there is an active search running or not. i.e. if
078: * searchString is null, there is no active search.
079: */
080: private String searchString = null;
081: private ListDataListener listModelListener;
082:
083: public ListSearcher(ListModel newModel) {
084: analyzer = new WhitespaceAnalyzer();
085: setListModel(newModel);
086: listModelListener = new ListModelHandler();
087: newModel.addListDataListener(listModelListener);
088: clearSearchingState();
089: }
090:
091: private void setListModel(ListModel newModel) {
092: //remove listeners if there...
093: if (newModel != null) {
094: newModel.removeListDataListener(listModelListener);
095: }
096:
097: listModel = newModel;
098: if (listModel != null) {
099: listModel.addListDataListener(listModelListener);
100: }
101:
102: //recalculate the links between this list model and
103: //the inner list model since the decorated model just changed
104: reindex();
105:
106: // let all listeners know the list has changed
107: fireContentsChanged(this , 0, getSize());
108: }
109:
110: private void reindex() {
111: try {
112: // recreate the RAMDirectory
113: directory = new RAMDirectory();
114: IndexWriter writer = new IndexWriter(directory, analyzer,
115: true);
116:
117: // iterate through all rows
118: for (int row = 0; row < listModel.getSize(); row++) {
119:
120: //for each row make a new document
121: Document document = new Document();
122: //add the row number of this row in the decorated list model
123: //this will allow us to retrive the results later
124: //and map this list model's row to a row in the decorated
125: //list model
126: document.add(new Field(ROW_NUMBER, "" + row,
127: Field.Store.YES, Field.Index.TOKENIZED));
128: //add the string representation of the row to the index
129: document.add(new Field(FIELD_NAME, String.valueOf(
130: listModel.getElementAt(row)).toLowerCase(),
131: Field.Store.YES, Field.Index.TOKENIZED));
132: writer.addDocument(document);
133: }
134: writer.optimize();
135: writer.close();
136: } catch (Exception e) {
137: e.printStackTrace();
138: }
139: }
140:
141: /**
142: * Run a new search.
143: *
144: * @param searchString Any valid lucene search string
145: */
146: public void search(String searchString) {
147:
148: //if search string is null or empty, clear the search == search all
149: if (searchString == null || searchString.equals("")) {
150: clearSearchingState();
151: fireContentsChanged(this , 0, getSize());
152: return;
153: }
154:
155: try {
156: //cache search String
157: this .searchString = searchString;
158:
159: //make a new index searcher with the in memory (RAM) index.
160: IndexSearcher is = new IndexSearcher(directory);
161:
162: //make an array of fields - one for each column
163: String[] fields = { FIELD_NAME };
164:
165: //build a query based on the fields, searchString and cached analyzer
166: //NOTE: This is an area for improvement since the MultiFieldQueryParser
167: // has some weirdness.
168: MultiFieldQueryParser parser = new MultiFieldQueryParser(
169: fields, analyzer);
170: Query query = parser.parse(searchString);
171: //run the search
172: Hits hits = is.search(query);
173: //reset this list model with the new results
174: resetSearchResults(hits);
175: } catch (Exception e) {
176: e.printStackTrace();
177: }
178:
179: //notify all listeners that the list has been changed
180: fireContentsChanged(this , 0, getSize());
181: }
182:
183: /**
184: *
185: * @param hits The new result set to set this list to.
186: */
187: private void resetSearchResults(Hits hits) {
188: try {
189: //clear our index mapping this list model rows to
190: //the decorated inner list model
191: rowToModelIndex.clear();
192: //iterate through the hits
193: //get the row number stored at the index
194: //that number is the row number of the decorated
195: //tabble model row that we are mapping to
196: for (int t = 0; t < hits.length(); t++) {
197: Document document = hits.doc(t);
198: Fieldable field = document.getField(ROW_NUMBER);
199: rowToModelIndex.add(new Integer(field.stringValue()));
200: }
201: } catch (Exception e) {
202: e.printStackTrace();
203: }
204: }
205:
206: /**
207: * @return The current lucene analyzer
208: */
209: public Analyzer getAnalyzer() {
210: return analyzer;
211: }
212:
213: /**
214: * @param analyzer The new analyzer to use
215: */
216: public void setAnalyzer(Analyzer analyzer) {
217: this .analyzer = analyzer;
218: //reindex from the model with the new analyzer
219: reindex();
220:
221: //rerun the search if there is an active search
222: if (isSearching()) {
223: search(searchString);
224: }
225: }
226:
227: private boolean isSearching() {
228: return searchString != null;
229: }
230:
231: private void clearSearchingState() {
232: searchString = null;
233: rowToModelIndex.clear();
234: for (int t = 0; t < listModel.getSize(); t++) {
235: rowToModelIndex.add(new Integer(t));
236: }
237: }
238:
239: private int getModelRow(int row) {
240: return ((Integer) rowToModelIndex.get(row)).intValue();
241: }
242:
243: public int getSize() {
244: return (listModel == null) ? 0 : rowToModelIndex.size();
245: }
246:
247: public Object getElementAt(int index) {
248: return listModel.getElementAt(getModelRow(index));
249: }
250:
251: class ListModelHandler implements ListDataListener {
252:
253: public void contentsChanged(ListDataEvent e) {
254: somethingChanged();
255: }
256:
257: public void intervalAdded(ListDataEvent e) {
258: somethingChanged();
259: }
260:
261: public void intervalRemoved(ListDataEvent e) {
262: somethingChanged();
263: }
264:
265: private void somethingChanged() {
266: // If we're not searching, just pass the event along.
267: if (!isSearching()) {
268: clearSearchingState();
269: reindex();
270: fireContentsChanged(ListSearcher.this , 0, getSize());
271: return;
272: }
273:
274: // Something has happened to the data that may have invalidated the search.
275: reindex();
276: search(searchString);
277: fireContentsChanged(ListSearcher.this , 0, getSize());
278: return;
279: }
280:
281: }
282:
283: }
|