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 }