001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.ac;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010
011import javax.swing.JTable;
012import javax.swing.table.AbstractTableModel;
013
014import org.openstreetmap.josm.tools.CheckParameterUtil;
015
016/**
017 * AutoCompletionList manages a list of {@link AutoCompletionListItem}s.
018 *
019 * The list is sorted, items with higher priority first, then according to lexicographic order
020 * on the value of the {@link AutoCompletionListItem}.
021 *
022 * AutoCompletionList maintains two views on the list of {@link AutoCompletionListItem}s.
023 * <ol>
024 *   <li>the bare, unfiltered view which includes all items</li>
025 *   <li>a filtered view, which includes only items which match a current filter expression</li>
026 * </ol>
027 *
028 * AutoCompletionList is an {@link AbstractTableModel} which serves the list of filtered
029 * items to a {@link JTable}.
030 * @since 1762
031 */
032public class AutoCompletionList extends AbstractTableModel {
033
034    /** the bare list of AutoCompletionItems */
035    private final transient List<AutoCompletionListItem> list;
036    /**  the filtered list of AutoCompletionItems */
037    private final transient ArrayList<AutoCompletionListItem> filtered;
038    /** the filter expression */
039    private String filter;
040    /** map from value to priority */
041    private final transient Map<String, AutoCompletionListItem> valutToItemMap;
042
043    /**
044     * constructor
045     */
046    public AutoCompletionList() {
047        list = new ArrayList<>();
048        filtered = new ArrayList<>();
049        valutToItemMap = new HashMap<>();
050    }
051
052    /**
053     * applies a filter expression to the list of {@link AutoCompletionListItem}s.
054     *
055     * The matching criterion is a case insensitive substring match.
056     *
057     * @param filter  the filter expression; must not be null
058     *
059     * @throws IllegalArgumentException if filter is null
060     */
061    public void applyFilter(String filter) {
062        CheckParameterUtil.ensureParameterNotNull(filter, "filter");
063        this.filter = filter;
064        filter();
065    }
066
067    /**
068     * clears the current filter
069     */
070    public void clearFilter() {
071        filter = null;
072        filter();
073    }
074
075    /**
076     * @return the current filter expression; null, if no filter expression is set
077     */
078    public String getFilter() {
079        return filter;
080    }
081
082    /**
083     * adds an AutoCompletionListItem to the list. Only adds the item if it
084     * is not null and if not in the list yet.
085     *
086     * @param item the item
087     */
088    public void add(AutoCompletionListItem item) {
089        if (item == null)
090            return;
091        appendOrUpdatePriority(item);
092        sort();
093        filter();
094    }
095
096    /**
097     * adds another AutoCompletionList to this list. An item is only
098     * added it is not null and if it does not exist in the list yet.
099     *
100     * @param other another auto completion list; must not be null
101     * @throws IllegalArgumentException if other is null
102     */
103    public void add(AutoCompletionList other) {
104        CheckParameterUtil.ensureParameterNotNull(other, "other");
105        for (AutoCompletionListItem item : other.list) {
106            appendOrUpdatePriority(item);
107        }
108        sort();
109        filter();
110    }
111
112    /**
113     * adds a list of AutoCompletionListItem to this list. Only items which
114     * are not null and which do not exist yet in the list are added.
115     *
116     * @param other a list of AutoCompletionListItem; must not be null
117     * @throws IllegalArgumentException if other is null
118     */
119    public void add(List<AutoCompletionListItem> other) {
120        CheckParameterUtil.ensureParameterNotNull(other, "other");
121        for (AutoCompletionListItem toadd : other) {
122            appendOrUpdatePriority(toadd);
123        }
124        sort();
125        filter();
126    }
127
128    /**
129     * adds a list of strings to this list. Only strings which
130     * are not null and which do not exist yet in the list are added.
131     *
132     * @param values a list of strings to add
133     * @param priority the priority to use
134     */
135    public void add(Collection<String> values, AutoCompletionItemPriority priority) {
136        if (values == null)
137            return;
138        for (String value : values) {
139            if (value == null) {
140                continue;
141            }
142            AutoCompletionListItem item = new AutoCompletionListItem(value, priority);
143            appendOrUpdatePriority(item);
144
145        }
146        sort();
147        filter();
148    }
149
150    /**
151     * Adds values that have been entered by the user.
152     * @param values values that have been entered by the user
153     */
154    public void addUserInput(Collection<String> values) {
155        if (values == null)
156            return;
157        int i = 0;
158        for (String value : values) {
159            if (value != null) {
160                appendOrUpdatePriority(
161                        new AutoCompletionListItem(value, new AutoCompletionItemPriority(false, false, false, i++)));
162            }
163        }
164        sort();
165        filter();
166    }
167
168    protected void appendOrUpdatePriority(AutoCompletionListItem toAdd) {
169        AutoCompletionListItem item = valutToItemMap.get(toAdd.getValue());
170        if (item == null) {
171            // new item does not exist yet. Add it to the list
172            list.add(toAdd);
173            valutToItemMap.put(toAdd.getValue(), toAdd);
174        } else {
175            item.setPriority(item.getPriority().mergeWith(toAdd.getPriority()));
176        }
177    }
178
179    /**
180     * checks whether a specific item is already in the list. Matches for the
181     * the value <strong>and</strong> the priority of the item
182     *
183     * @param item the item to check
184     * @return true, if item is in the list; false, otherwise
185     */
186    public boolean contains(AutoCompletionListItem item) {
187        if (item == null)
188            return false;
189        return list.contains(item);
190    }
191
192    /**
193     * checks whether an item with the given value is already in the list. Ignores
194     * priority of the items.
195     *
196     * @param value the value of an auto completion item
197     * @return true, if value is in the list; false, otherwise
198     */
199    public boolean contains(String value) {
200        if (value == null)
201            return false;
202        for (AutoCompletionListItem item: list) {
203            if (item.getValue().equals(value))
204                return true;
205        }
206        return false;
207    }
208
209    /**
210     * removes the auto completion item with key <code>key</code>
211     * @param key  the key;
212     */
213    public void remove(String key) {
214        if (key == null)
215            return;
216        for (int i = 0; i < list.size(); i++) {
217            AutoCompletionListItem item = list.get(i);
218            if (item.getValue().equals(key)) {
219                list.remove(i);
220                return;
221            }
222        }
223    }
224
225    /**
226     * sorts the list
227     */
228    protected void sort() {
229        Collections.sort(list);
230    }
231
232    protected void filter() {
233        filtered.clear();
234        if (filter == null) {
235            // Collections.copy throws an exception "Source does not fit in dest"
236            filtered.ensureCapacity(list.size());
237            for (AutoCompletionListItem item: list) {
238                filtered.add(item);
239            }
240            return;
241        }
242
243        // apply the pattern to list of possible values. If it matches, add the
244        // value to the list of filtered values
245        //
246        for (AutoCompletionListItem item : list) {
247            if (item.getValue().startsWith(filter)) {
248                filtered.add(item);
249            }
250        }
251        fireTableDataChanged();
252    }
253
254    /**
255     * replies the number of filtered items
256     *
257     * @return the number of filtered items
258     */
259    public int getFilteredSize() {
260        return this.filtered.size();
261    }
262
263    /**
264     * replies the idx-th item from the list of filtered items
265     * @param idx the index; must be in the range 0 &lt;= idx &lt; {@link #getFilteredSize()}
266     * @return the item
267     *
268     * @throws IndexOutOfBoundsException if idx is out of bounds
269     */
270    public AutoCompletionListItem getFilteredItem(int idx) {
271        if (idx < 0 || idx >= getFilteredSize())
272            throw new IndexOutOfBoundsException("idx out of bounds. idx=" + idx);
273        return filtered.get(idx);
274    }
275
276    List<AutoCompletionListItem> getList() {
277        return list;
278    }
279
280    List<AutoCompletionListItem> getUnmodifiableList() {
281        return Collections.unmodifiableList(list);
282    }
283
284    /**
285     * removes all elements from the auto completion list
286     *
287     */
288    public void clear() {
289        valutToItemMap.clear();
290        list.clear();
291        fireTableDataChanged();
292    }
293
294    @Override
295    public int getColumnCount() {
296        return 1;
297    }
298
299    @Override
300    public int getRowCount() {
301
302        return list == null ? 0 : getFilteredSize();
303    }
304
305    @Override
306    public Object getValueAt(int rowIndex, int columnIndex) {
307        return list == null ? null : getFilteredItem(rowIndex);
308    }
309}