View Javadoc

1   /*
2    * Copyright (c) 2005-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.danube.http.util;
14  
15  import java.util.ArrayList;
16  import java.util.Arrays;
17  import java.util.Collection;
18  import java.util.Collections;
19  import java.util.HashMap;
20  import java.util.List;
21  import java.util.Map;
22  import java.util.Set;
23  
24  /**
25   * {@link MultiStringMap} implementation using {@link java.util.HashMap}
26   * This implementation uses map and is storing multiple elements in a list.
27   * If only one element remains in the list
28   * then it is replaced with that element only.
29   *
30   * @author Daniel Sendula
31   */
32  public class MultiStringHashMap implements MultiStringMap {
33  
34      /** Backing storage. */
35      protected Map<String, Object> map;
36  
37      /**
38       * Constructor.
39       */
40      public MultiStringHashMap() {
41          map = new HashMap<String, Object>();
42      }
43  
44      /**
45       * Constructor.
46       * @param initialCapacity initial capacity of a backing map
47       */
48      public MultiStringHashMap(int initialCapacity) {
49          map = new HashMap<String, Object>(initialCapacity);
50      }
51  
52      /**
53       * Constructor.
54       * @param initialCapacity initial capacity of a backing map
55       * @param loadFactor load factory of a backing map
56       */
57      public MultiStringHashMap(int initialCapacity, float loadFactor) {
58          map = new HashMap<String, Object>(initialCapacity, loadFactor);
59      }
60  
61      /**
62       * <p>
63       *   Adds new element to the map. If no entries with given id exist than this is going to be the first.
64       *   If there are already values under given id then this is going to be added as a new one.
65       * </p>
66       * <p>
67       *   This implementation if there is already one value in the backing map converts it to a list and adds both (old and new values) to it.
68       * </p>
69       * @param id key
70       * @param value value to be added
71       * @throws IllegalStateException if existing element of the backing storage is not a string or a list
72       */
73      @SuppressWarnings("unchecked")
74      public void add(String id, String value) {
75          Object old = map.get(id);
76          if (old == null) {
77              map.put(id, value);
78          } else if (old instanceof String) {
79              ArrayList<String> list = new ArrayList<String>();
80              list.add((String)old);
81              list.add(value);
82          } else if (old instanceof List) {
83              ((List<String>)old).add(value);
84          } else {
85              throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
86          }
87      }
88  
89      /**
90       * <p>Adds all elements from the given array.</p>
91       * <p>This implementation is using {@link MultiStringHashMap#addAll(String, Collection)} to add values</p>
92       *
93       * @param id key
94       * @param values array of values
95       * @see MultiStringMap#add(String, String)
96       */
97      public void addAll(String id, String[] values) {
98          addAll(id, Arrays.asList(values));
99      }
100 
101     /**
102      * <p>Adds all elements from the given collection</p>
103      * <p>This implementation if there is already one value in the backing map converts it to a list and adds both (old and new values) to it.</p>
104      *
105      * @param id key
106      * @param values collection which elements are to be added
107      * @throws IllegalStateException if existing element of the backing storage is not a string or a list
108      * @see MultiStringMap#add(String, String)
109      */
110     @SuppressWarnings("unchecked")
111     public void addAll(String id, Collection<String> values) {
112         Object old = map.get(id);
113         if (old == null) {
114             ArrayList<String> list = new ArrayList<String>();
115             list.addAll(values);
116             map.put(id, list);
117         } else if (old instanceof String) {
118             ArrayList<String> list = new ArrayList<String>();
119             list.add((String)old);
120             list.addAll(values);
121             map.put(id, list);
122         } else if (old instanceof List) {
123             ((List<String>)old).addAll(values);
124         } else {
125             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
126         }
127     }
128 
129     /**
130      * Replaces existing element(s), if there are any under the given key, with the given value.
131      * @param id key
132      * @param value value to be put to the map
133      */
134     public void putOnly(String id, String value) {
135         map.put(id, value);
136     }
137 
138     /**
139      * <p>Replaces existing element(s), if there are any under the given key, with the given values.</p>
140      * <p>
141      *   If array's length is greater then one it stores given array of strings as a list, if arrayls.
142      *   If array's length is exactly one then value of it is stored as a string while
143      *   if array's length is zero then it is removed from the backing storage.
144      * </p>
145      *
146      * @param id key
147      * @param values values to be put to the map
148      * @see MultiStringMap#putOnly(String, String)
149      */
150     public void putAll(String id, String[] values) {
151         if (values.length > 1) {
152             putAll(id, Arrays.asList(values));
153         } else if (values.length == 1) {
154             putOnly(id, values[0]);
155         } else {
156             removeAll(id);
157         }
158     }
159 
160     /**
161      * <p>
162      *   Replaces existing element(s), if there are any under the given key, with the given values.
163      * </p>
164      * <p>
165      *   If array's length is greater then one it stores given array of strings as a list, if arrayls.
166      *   If array's length is exactly one then value of it is stored as a string while
167      *   if array's length is zero then it is removed from the backing storage.
168      * </p>
169      * <p>
170      *   Note: This implementation is not ensuring that all collection elements are of {@link java.lang.String} type.
171      * </p>
172      *
173      * @param id key
174      * @param values collection of values to be put to the map
175      * @see MultiStringMap#putOnly(String, String)
176      */
177     public void putAll(String id, Collection<String> values) {
178         ArrayList<String> list = null;
179         if (values instanceof ArrayList) {
180             list = (ArrayList<String>)values;
181             map.put(id, values);
182         } else {
183             list = new ArrayList<String>();
184             list.addAll(values);
185             map.put(id, values);
186         }
187         int size = list.size();
188         if (size == 0) {
189             removeAll(id);
190         } else if (size == 1) {
191             putOnly(id, (String)list.get(0));
192         } else {
193             map.put(id, list);
194         }
195     }
196 
197     /**
198      * Removes all elements from the given key
199      * @param id key
200      */
201     @SuppressWarnings("unchecked")
202     public Collection<String> removeAll(String id) {
203         Object old = map.remove(id);
204         if (old instanceof String) {
205             String[] res = new String[1];
206             res[0] = (String)old;
207             return Collections.unmodifiableList(Arrays.asList(res));
208         } else if (old instanceof List) {
209             return Collections.unmodifiableList((List<String>)old);
210         } else if (old == null) {
211             return null;
212         } else {
213             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
214         }
215     }
216 
217     /**
218      * <p>
219      *   Removes first element from the given key. If there was only one element then there will be no more elements under
220      *   the given key
221      * </p>
222      * <p>
223      *   Implementation ensures that if list is reduced to only one element then that element is stored instead of the list itself.
224      * </p>
225      * @param id key
226      * @throws IllegalStateException if the element of the backing storage is not string nor list or there are no elements to be removed
227      */
228     @SuppressWarnings("unchecked")
229     public String removeFirst(String id) {
230         Object old = map.get(id);
231         if (old instanceof String) {
232             return (String)map.remove(id);
233         } else if (old instanceof List) {
234             List<String> list = (List<String>)old;
235             String oldString = list.remove(0);
236             if (list.size() == 1) {
237                 map.put(id, list.get(0));
238             }
239             return oldString;
240         } else {
241             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
242         }
243     }
244 
245     /**
246      * <p>
247      *   Removes n-th element from the given key.
248      * </p>
249      * <p>
250      *   Implementation ensures that if list is reduced to only one element then that element is stored instead of the list itself.
251      * </p>
252      * @param id key
253      * @param index index of the element to be removed
254      * @throws IndexOutOfBoundsException if there are no elements under the given key
255      * @throws IllegalStateException if the element of the backing storage is not string nor list or there are no elements to be removed
256      */
257     @SuppressWarnings("unchecked")
258     public String remove(String id, int index) {
259         Object old = map.get(id);
260         if (old instanceof String) {
261             if (index == 0) {
262                 return (String)map.remove(id);
263             } else {
264                 throw new IndexOutOfBoundsException("Has one element but index is " + index);
265             }
266         } else if (old instanceof List) {
267             List<String> list = (List<String>)old;
268             String oldString = list.remove(index);
269             if (list.size() == 1) {
270                 map.put(id, list.get(0));
271             }
272             return oldString;
273         } else {
274             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
275         }
276     }
277 
278     /**
279      * Returns <code>true</code> if there is at least one entry
280      * @param id key
281      * @return <code>true</code> if there is at least one entry
282      */
283     public boolean containsKey(String id) {
284         return map.containsKey(id);
285     }
286 
287     /**
288      * Returns number of entries for given key
289      * @param id key
290      * @return number of entries for given key
291      */
292     @SuppressWarnings("unchecked")
293     public int getEntrySize(String id) {
294         Object old = map.get(id);
295         if (old instanceof String) {
296             return 1;
297         } else if (old instanceof List) {
298             List<String> list = (List<String>)old;
299             return list.size();
300         } else if (old == null) {
301             return 0;
302         } else {
303             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
304         }
305     }
306 
307     /**
308      * Retrieves element from the given key. Key must have only one element (or none) for this method to work.
309      * @param id key of the asked element
310      * @return element
311      * @throws IllegalStateException if there are more then one element under this key or nor string or list is stored in the backing storage
312      */
313     @SuppressWarnings("unchecked")
314     public String getOnly(String id) {
315         Object old = map.get(id);
316         if (old instanceof String) {
317             return (String)map.get(id);
318         } else if (old instanceof List) {
319             List<String> list = (List<String>)old;
320             int size = list.size();
321             if (size == 0) {
322                 return null;
323             } else if (size == 1) {
324                 return list.get(0);
325             } else {
326                 // TODO need better exception here
327                 throw new IllegalStateException("Asked key has more then one element");
328             }
329         } else if (old == null) {
330             return null;
331         } else {
332             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
333         }
334     }
335 
336     /**
337      * Retrieves element from the given key. Key must have only one element (or none) for this method to work.
338      * @param id key of the asked element
339      * @return element
340      * @throws IllegalStateException if there are more then one element under this key or nor string or list is stored in the backing storage
341      */
342     @SuppressWarnings("unchecked")
343     public String getFirst(String id) {
344         Object old = map.get(id);
345         if (old instanceof String) {
346             return (String)map.get(id);
347         } else if (old instanceof List) {
348             List<String> list = (List<String>)old;
349             int size = list.size();
350             if (size == 0) {
351                 return null;
352             } else {
353                 return list.get(0);
354             }
355         } else if (old == null) {
356             return null;
357         } else {
358             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
359         }
360     }
361 
362     /**
363      * Returns array of all elements under asked key. If there are no elements then it returns an empty array.
364      * @param id key of the asked elements
365      * @return an array of strings
366      * @throws IllegalStateException if the element of the backing storage is not string nor list or there are no elements to be removed
367      */
368     @SuppressWarnings("unchecked")
369     public String[] getAsArray(String id) {
370         Object old = map.get(id);
371         if (old == null) {
372             return new String[0];
373         } else if (old instanceof String) {
374             return new String[]{(String)old};
375         } else if (old instanceof List) {
376             List<String> list = (List<String>)old;
377             String[] res = new String[list.size()];
378             res = list.toArray(res);
379             return res;
380         } else {
381             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
382         }
383     }
384 
385     /**
386      * Returns list of all elements under asked key. If there are no elements then it returns an empty list.
387      * @param id key of the asked elements
388      * @return a list of strings
389      * @throws IllegalStateException if the element of the backing storage is not string nor list or there are no elements to be removed
390      */
391     @SuppressWarnings("unchecked")
392     public List<String> getAsList(String id) {
393         Object old = map.get(id);
394         if (old == null) {
395             return Collections.emptyList();
396         } else if (old instanceof String) {
397             return Arrays.asList((String)old);
398         } else if (old instanceof List) {
399             return (List<String>)old;
400         } else {
401             throw new IllegalStateException("Found wrong type as the maps value; " + old.getClass());
402         }
403     }
404 
405     /**
406      * Clears the map
407      */
408     public void clear() {
409         map.clear();
410     }
411 
412     /**
413      * Returns key set
414      * @return key set
415      */
416     public Set<String> keySet() {
417         return map.keySet();
418     }
419 
420     /**
421      * Returns a map that contains all elements. Where there are more elements per key then implementation is responsiple of
422      * returning something sensible like an array of strings or a collection (or a list) whose elements are strings
423      * @return a map
424      */
425     public Map<String, Object> getAsMap() {
426         return map;
427     }
428 
429     /**
430      * Returns list of all entries. Entries with the same key will repeat if there are more elements stored under the same key.
431      * @return list of all entries
432      * @throws IllegalStateException if the element of the backing storage is not string nor list or there are no elements to be removed
433      */
434     @SuppressWarnings("unchecked")
435     public Collection<Map.Entry<String, String>> getAllEntries() {
436         ArrayList<Map.Entry<String, String>> all = new ArrayList<Map.Entry<String, String>>();
437 
438         for (Map.Entry<String, Object> entry : map.entrySet()) {
439             Object obj = entry.getValue();
440             if (obj instanceof String) {
441                 InternalMapEntry<String, String> newEntry = new InternalMapEntry<String, String>(entry.getKey(), (String)entry.getValue());
442                 all.add(newEntry);
443                 // all.add(entry);
444             } else if (obj instanceof List) {
445                 List<String> list = (List<String>)obj;
446                 String key = entry.getKey();
447                 for (String element : list) {
448                     InternalMapEntry<String, String> newEntry = new InternalMapEntry<String, String>(key, element);
449                     all.add(newEntry);
450                 }
451             } else {
452                 throw new IllegalStateException("Found wrong type as the maps value; " + obj.getClass());
453             }
454         }
455 
456         return (Collection<Map.Entry<String, String>>)all;
457     }
458 
459     /**
460      * Number of keys in the map. It won't return total number of elements but just number of differnet keys in the map.
461      * @return number of keys in the map
462      */
463     public int size() {
464         return map.size();
465     }
466 
467     /**
468      * Map entry for internal use
469      */
470     protected class InternalMapEntry<K, V> implements Map.Entry<K, V> {
471 
472         /** Key */
473         protected K key;
474         /** Value */
475         protected V value;
476 
477         /**
478          * Constructor
479          * @param key key
480          * @param value value
481          */
482         public InternalMapEntry(K key, V value) {
483             this.key = key;
484             this.value = value;
485         }
486 
487         /**
488          * Returns key
489          * @return key
490          */
491         public K getKey() {
492             return key;
493         }
494 
495         /**
496          * Returns value
497          * @return value
498          */
499         public V getValue() {
500             return value;
501         }
502 
503         /**
504          * Sets value
505          * @param value value
506          * @return old value
507          */
508         public V setValue(V value) {
509             V old = this.value;
510             this.value = value;
511             return old;
512         }
513 
514     }
515 
516 }