001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it
010     * under the terms of the GNU Lesser General Public License as published by
011     * the Free Software Foundation; either version 2.1 of the License, or
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022     * USA.
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * -----------------------
028     * DefaultKeyedValues.java
029     * -----------------------
030     * (C) Copyright 2002-2008, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Thomas Morgner;
034     *
035     * Changes:
036     * --------
037     * 31-Oct-2002 : Version 1 (DG);
038     * 11-Feb-2003 : Fixed bug in getValue(key) method for unrecognised key (DG);
039     * 05-Mar-2003 : Added methods to sort stored data 'by key' or 'by value' (DG);
040     * 13-Mar-2003 : Implemented Serializable (DG);
041     * 08-Apr-2003 : Modified removeValue(Comparable) method to fix bug 717049 (DG);
042     * 18-Aug-2003 : Implemented Cloneable (DG);
043     * 27-Aug-2003 : Moved SortOrder from org.jfree.data --> org.jfree.util (DG);
044     * 09-Feb-2004 : Modified getIndex() method - see bug report 893256 (DG);
045     * 15-Sep-2004 : Updated clone() method and added PublicCloneable
046     *               interface (DG);
047     * 25-Nov-2004 : Small update to the clone() implementation (DG);
048     * 24-Feb-2005 : Added methods addValue(Comparable, double) and
049     *               setValue(Comparable, double) for convenience (DG);
050     * ------------- JFREECHART 1.0.x ---------------------------------------------
051     * 31-Jul-2006 : Added a clear() method (DG);
052     * 01-Aug-2006 : Added argument check to getIndex() method (DG);
053     * 30-Apr-2007 : Added insertValue() methods (DG);
054     * 31-Oct-2007 : Performance improvements by using separate lists for keys and
055     *               values (TM);
056     * 21-Nov-2007 : Fixed bug in removeValue() method from previous patch (DG);
057     *
058     */
059    
060    package org.jfree.data;
061    
062    import java.io.Serializable;
063    import java.util.ArrayList;
064    import java.util.Arrays;
065    import java.util.Comparator;
066    import java.util.HashMap;
067    import java.util.List;
068    
069    import org.jfree.util.PublicCloneable;
070    import org.jfree.util.SortOrder;
071    
072    /**
073     * An ordered list of (key, value) items.  This class provides a default
074     * implementation of the {@link KeyedValues} interface.
075     */
076    public class DefaultKeyedValues implements KeyedValues, Cloneable,
077            PublicCloneable, Serializable {
078    
079        /** For serialization. */
080        private static final long serialVersionUID = 8468154364608194797L;
081    
082        /** Storage for the keys. */
083        private ArrayList keys;
084    
085        /** Storage for the values. */
086        private ArrayList values;
087    
088        /**
089         * Contains (key, Integer) mappings, where the Integer is the index for
090         * the key in the list.
091         */
092        private HashMap indexMap;
093    
094      /**
095         * Creates a new collection (initially empty).
096         */
097        public DefaultKeyedValues() {
098            this.keys = new ArrayList();
099            this.values = new ArrayList();
100            this.indexMap = new HashMap();
101        }
102    
103        /**
104         * Returns the number of items (values) in the collection.
105         *
106         * @return The item count.
107         */
108        public int getItemCount() {
109            return this.indexMap.size();
110        }
111    
112        /**
113         * Returns a value.
114         *
115         * @param item  the item of interest (zero-based index).
116         *
117         * @return The value (possibly <code>null</code>).
118         *
119         * @throws IndexOutOfBoundsException if <code>item</code> is out of bounds.
120         */
121        public Number getValue(int item) {
122            return (Number) this.values.get(item);
123        }
124    
125        /**
126         * Returns a key.
127         *
128         * @param index  the item index (zero-based).
129         *
130         * @return The row key.
131         *
132         * @throws IndexOutOfBoundsException if <code>item</code> is out of bounds.
133         */
134        public Comparable getKey(int index) {
135            return (Comparable) this.keys.get(index);
136        }
137    
138        /**
139         * Returns the index for a given key.
140         *
141         * @param key  the key (<code>null</code> not permitted).
142         *
143         * @return The index, or <code>-1</code> if the key is not recognised.
144         *
145         * @throws IllegalArgumentException if <code>key</code> is
146         *     <code>null</code>.
147         */
148        public int getIndex(Comparable key) {
149            if (key == null) {
150                throw new IllegalArgumentException("Null 'key' argument.");
151            }
152            final Integer i = (Integer) this.indexMap.get(key);
153            if (i == null) {
154                return -1;  // key not found
155            }
156            return i.intValue();
157        }
158    
159        /**
160         * Returns the keys for the values in the collection.
161         *
162         * @return The keys (never <code>null</code>).
163         */
164        public List getKeys() {
165            return (List) this.keys.clone();
166        }
167    
168        /**
169         * Returns the value for a given key.
170         *
171         * @param key  the key (<code>null</code> not permitted).
172         *
173         * @return The value (possibly <code>null</code>).
174         *
175         * @throws UnknownKeyException if the key is not recognised.
176         *
177         * @see #getValue(int)
178         */
179        public Number getValue(Comparable key) {
180            int index = getIndex(key);
181            if (index < 0) {
182                throw new UnknownKeyException("Key not found: " + key);
183            }
184            return getValue(index);
185        }
186    
187        /**
188         * Updates an existing value, or adds a new value to the collection.
189         *
190         * @param key  the key (<code>null</code> not permitted).
191         * @param value  the value.
192         *
193         * @see #addValue(Comparable, Number)
194         */
195        public void addValue(Comparable key, double value) {
196            addValue(key, new Double(value));
197        }
198    
199        /**
200         * Adds a new value to the collection, or updates an existing value.
201         * This method passes control directly to the
202         * {@link #setValue(Comparable, Number)} method.
203         *
204         * @param key  the key (<code>null</code> not permitted).
205         * @param value  the value (<code>null</code> permitted).
206         */
207        public void addValue(Comparable key, Number value) {
208            setValue(key, value);
209        }
210    
211        /**
212         * Updates an existing value, or adds a new value to the collection.
213         *
214         * @param key  the key (<code>null</code> not permitted).
215         * @param value  the value.
216         */
217        public void setValue(Comparable key, double value) {
218            setValue(key, new Double(value));
219        }
220    
221        /**
222         * Updates an existing value, or adds a new value to the collection.
223         *
224         * @param key  the key (<code>null</code> not permitted).
225         * @param value  the value (<code>null</code> permitted).
226         */
227        public void setValue(Comparable key, Number value) {
228            if (key == null) {
229                throw new IllegalArgumentException("Null 'key' argument.");
230            }
231            int keyIndex = getIndex(key);
232            if (keyIndex >= 0) {
233                this.keys.set(keyIndex, key);
234                this.values.set(keyIndex, value);
235            }
236            else {
237                this.keys.add(key);
238                this.values.add(value);
239                this.indexMap.put(key, new Integer(this.keys.size() - 1));
240            }
241        }
242    
243        /**
244         * Inserts a new value at the specified position in the dataset or, if
245         * there is an existing item with the specified key, updates the value
246         * for that item and moves it to the specified position.
247         *
248         * @param position  the position (in the range 0 to getItemCount()).
249         * @param key  the key (<code>null</code> not permitted).
250         * @param value  the value.
251         *
252         * @since 1.0.6
253         */
254        public void insertValue(int position, Comparable key, double value) {
255            insertValue(position, key, new Double(value));
256        }
257    
258        /**
259         * Inserts a new value at the specified position in the dataset or, if
260         * there is an existing item with the specified key, updates the value
261         * for that item and moves it to the specified position.
262         *
263         * @param position  the position (in the range 0 to getItemCount()).
264         * @param key  the key (<code>null</code> not permitted).
265         * @param value  the value (<code>null</code> permitted).
266         *
267         * @since 1.0.6
268         */
269        public void insertValue(int position, Comparable key, Number value) {
270            if (position < 0 || position > getItemCount()) {
271                throw new IllegalArgumentException("'position' out of bounds.");
272            }
273            if (key == null) {
274                throw new IllegalArgumentException("Null 'key' argument.");
275            }
276            int pos = getIndex(key);
277            if (pos == position) {
278                this.keys.set(pos, key);
279                this.values.set(pos, value);
280            }
281            else {
282                if (pos >= 0) {
283                    this.keys.remove(pos);
284                    this.values.remove(pos);
285                }
286    
287                this.keys.add(position, key);
288                this.values.add(position, value);
289                rebuildIndex();
290            }
291        }
292    
293        /**
294         * Rebuilds the key to indexed-position mapping after an positioned insert
295         * or a remove operation.
296         */
297        private void rebuildIndex () {
298            this.indexMap.clear();
299            for (int i = 0; i < this.keys.size(); i++) {
300                final Object key = this.keys.get(i);
301                this.indexMap.put(key, new Integer(i));
302            }
303        }
304    
305        /**
306         * Removes a value from the collection.
307         *
308         * @param index  the index of the item to remove (in the range
309         *     <code>0</code> to <code>getItemCount() - 1</code>).
310         *
311         * @throws IndexOutOfBoundsException if <code>index</code> is not within
312         *     the specified range.
313         */
314        public void removeValue(int index) {
315            this.keys.remove(index);
316            this.values.remove(index);
317            rebuildIndex();
318        }
319    
320        /**
321         * Removes a value from the collection.
322         *
323         * @param key  the item key (<code>null</code> not permitted).
324         *
325         * @throws IllegalArgumentException if <code>key</code> is
326         *     <code>null</code>.
327         * @throws UnknownKeyException if <code>key</code> is not recognised.
328         */
329        public void removeValue(Comparable key) {
330            int index = getIndex(key);
331            if (index < 0) {
332                throw new UnknownKeyException("The key (" + key
333                        + ") is not recognised.");
334            }
335            removeValue(index);
336        }
337    
338        /**
339         * Clears all values from the collection.
340         *
341         * @since 1.0.2
342         */
343        public void clear() {
344            this.keys.clear();
345            this.values.clear();
346            this.indexMap.clear();
347        }
348    
349        /**
350         * Sorts the items in the list by key.
351         *
352         * @param order  the sort order (<code>null</code> not permitted).
353         */
354        public void sortByKeys(SortOrder order) {
355            final int size = this.keys.size();
356            final DefaultKeyedValue[] data = new DefaultKeyedValue[size];
357    
358            for (int i = 0; i < size; i++) {
359                data[i] = new DefaultKeyedValue((Comparable) this.keys.get(i),
360                        (Number) this.values.get(i));
361            }
362    
363            Comparator comparator = new KeyedValueComparator(
364                    KeyedValueComparatorType.BY_KEY, order);
365            Arrays.sort(data, comparator);
366            clear();
367    
368            for (int i = 0; i < data.length; i++) {
369                final DefaultKeyedValue value = data[i];
370                addValue(value.getKey(), value.getValue());
371            }
372        }
373    
374        /**
375         * Sorts the items in the list by value.  If the list contains
376         * <code>null</code> values, they will sort to the end of the list,
377         * irrespective of the sort order.
378         *
379         * @param order  the sort order (<code>null</code> not permitted).
380         */
381        public void sortByValues(SortOrder order) {
382            final int size = this.keys.size();
383            final DefaultKeyedValue[] data = new DefaultKeyedValue[size];
384            for (int i = 0; i < size; i++) {
385                data[i] = new DefaultKeyedValue((Comparable) this.keys.get(i),
386                        (Number) this.values.get(i));
387            }
388    
389            Comparator comparator = new KeyedValueComparator(
390                    KeyedValueComparatorType.BY_VALUE, order);
391            Arrays.sort(data, comparator);
392    
393            clear();
394            for (int i = 0; i < data.length; i++) {
395                final DefaultKeyedValue value = data[i];
396                addValue(value.getKey(), value.getValue());
397            }
398        }
399    
400        /**
401         * Tests if this object is equal to another.
402         *
403         * @param obj  the object (<code>null</code> permitted).
404         *
405         * @return A boolean.
406         */
407        public boolean equals(Object obj) {
408            if (obj == this) {
409                return true;
410            }
411    
412            if (!(obj instanceof KeyedValues)) {
413                return false;
414            }
415    
416            KeyedValues that = (KeyedValues) obj;
417            int count = getItemCount();
418            if (count != that.getItemCount()) {
419                return false;
420            }
421    
422            for (int i = 0; i < count; i++) {
423                Comparable k1 = getKey(i);
424                Comparable k2 = that.getKey(i);
425                if (!k1.equals(k2)) {
426                    return false;
427                }
428                Number v1 = getValue(i);
429                Number v2 = that.getValue(i);
430                if (v1 == null) {
431                    if (v2 != null) {
432                        return false;
433                    }
434                }
435                else {
436                    if (!v1.equals(v2)) {
437                        return false;
438                    }
439                }
440            }
441            return true;
442        }
443    
444        /**
445         * Returns a hash code.
446         *
447         * @return A hash code.
448         */
449        public int hashCode() {
450            return (this.keys != null ? this.keys.hashCode() : 0);
451        }
452    
453        /**
454         * Returns a clone.
455         *
456         * @return A clone.
457         *
458         * @throws CloneNotSupportedException  this class will not throw this
459         *         exception, but subclasses might.
460         */
461        public Object clone() throws CloneNotSupportedException {
462            DefaultKeyedValues clone = (DefaultKeyedValues) super.clone();
463            clone.keys = (ArrayList) this.keys.clone();
464            clone.values = (ArrayList) this.values.clone();
465            clone.indexMap = (HashMap) this.indexMap.clone();
466            return clone;
467        }
468    
469    }