001 /*
002 * Copyright 2002-2004 The Apache Software Foundation
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.apache.commons.clazz.reflect.extended;
017
018 import java.lang.reflect.Array;
019 import java.lang.reflect.InvocationTargetException;
020 import java.lang.reflect.Method;
021 import java.util.AbstractMap;
022 import java.util.AbstractSet;
023 import java.util.Arrays;
024 import java.util.Collection;
025 import java.util.Collections;
026 import java.util.HashMap;
027 import java.util.HashSet;
028 import java.util.Iterator;
029 import java.util.Map;
030 import java.util.Set;
031
032 import org.apache.commons.clazz.ClazzAccessException;
033
034
035 /**
036 * This is an implementation of the <code>Map</code> interface
037 * that is based on a Mapped property. Whenever possible, it
038 * uses concrete methods on the owner of the property to manipulate the map.
039 * <p>
040 * Consider the following example:
041 * <pre>
042 * Map map = (Map)clazz.getProperty("fooMap").get(instance);
043 * Object value = map.get("bar");
044 * </pre>
045 *
046 * If <code>instance</code> has a <code>getFoo(String key)</code> method,
047 * this code will implicitly invoke it like this: <code>getFoo("bar")</code>.
048 * otherwise it will obtain the whole map and extract the
049 * requested value.
050 *
051 * @author <a href="mailto:dmitri@apache.org">Dmitri Plotnikov</a>
052 * @version $Id: ReflectedMap.java 155436 2005-02-26 13:17:48Z dirkv $
053 */
054 public class ReflectedMap extends AbstractMap {
055 private Object instance;
056 private ReflectedMappedProperty property;
057 private int modCount = 0;
058
059 /**
060 * Constructor for ReflectedMap.
061 */
062 public ReflectedMap(
063 Object instance,
064 ReflectedMappedProperty property)
065 {
066 this.instance = instance;
067 this.property = property;
068 }
069
070 public Map getPropertyValue() {
071 Method readMethod = property.getReadMethod();
072 if (readMethod == null) {
073 throw new ClazzAccessException(
074 "Cannot read property "
075 + property.getName()
076 + ": no read method");
077 }
078 try {
079 return (Map) readMethod.invoke(instance, null);
080 }
081 catch (Exception ex) {
082 throw accessException("Cannot read property", readMethod, ex);
083 }
084 }
085
086 public void setPropertyValue(Map value) {
087 Method writeMethod = property.getWriteMethod();
088 if (writeMethod == null) {
089 throw new ClazzAccessException(
090 "Cannot set property: "
091 + property.getName()
092 + ": no set(array) method");
093 }
094
095 try {
096 writeMethod.invoke(instance, new Object[] { value });
097 }
098 catch (Exception ex) {
099 throw accessException("Cannot set property", writeMethod, ex);
100 }
101 }
102
103 public Set getPropertyKeySet() {
104 Method keySetMethod = property.getKeySetMethod();
105 if (keySetMethod != null) {
106 Set set;
107 try {
108 Object value = keySetMethod.invoke(instance, null);
109 if (value == null) {
110 set = Collections.EMPTY_SET;
111 }
112 else if (value instanceof Set) {
113 set = (Set) value;
114 }
115 else if (value instanceof Collection) {
116 set = new ConcurrentChangeSafeSet((Collection) value);
117 }
118 else {
119 set =
120 new ConcurrentChangeSafeSet(
121 Arrays.asList((Object[]) value));
122 }
123 }
124 catch (Exception ex) {
125 throw new ClazzAccessException(
126 "Cannot get key set: "
127 + property.getName()
128 + ": cannot invoke method: "
129 + keySetMethod.getName(),
130 ex);
131 }
132 return set;
133 }
134 else {
135 Map map = getPropertyValue();
136 if (map == null) {
137 return Collections.EMPTY_SET;
138 }
139 return map.keySet();
140 }
141 }
142
143 /**
144 * If there is a getFoo(key) method, calls that for every key.
145 * Otherwise, calls getFooMap().get(key)
146 *
147 * @see java.util.Map#get(java.lang.Object)
148 */
149 public Object get(Object key) {
150 Method getMethod = property.getGetMethod();
151 if (getMethod != null) {
152 Object value;
153 try {
154 value = getMethod.invoke(instance, new Object[]{key});
155 }
156 catch (Exception ex) {
157 throw new ClazzAccessException(
158 "Cannot get property : "
159 + property.getName()
160 + ": cannot invoke method: "
161 + getMethod.getName(),
162 ex);
163 }
164 return value;
165 }
166 else {
167 Map map = getPropertyValue();
168 if (map == null) {
169 return null;
170 }
171 return map.get(key);
172 }
173 }
174
175 /**
176 * @see java.util.Map#size()
177 */
178 public int size() {
179 Method keySetMethod = property.getKeySetMethod();
180 if (keySetMethod != null) {
181 try {
182 Object value = keySetMethod.invoke(instance, null);
183 if (value == null) {
184 return 0;
185 }
186 else if (value instanceof Collection) {
187 return ((Collection) value).size();
188 }
189 else {
190 return Array.getLength(value);
191 }
192 }
193 catch (Exception ex) {
194 throw accessException("Cannot get key set", keySetMethod, ex);
195 }
196 }
197 else {
198 Map map = getPropertyValue();
199 if (map == null) {
200 return 0;
201 }
202 return map.size();
203 }
204 }
205
206 /**
207 * @see java.util.Map#isEmpty()
208 */
209 public boolean isEmpty() {
210 return size() == 0;
211 }
212
213 /**
214 * @see java.util.Map#keySet()
215 */
216 public Set keySet() {
217 return new EntrySet(KEYS);
218 }
219
220 /**
221 * @see java.util.Map#entrySet()
222 */
223 public Set entrySet() {
224 return new EntrySet(ENTRIES);
225 }
226
227 /**
228 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
229 */
230 public Object put(Object key, Object value) {
231 Method putMethod = property.getPutMethod();
232 if (putMethod != null) {
233 Object oldValue = null;
234 try {
235 oldValue = get(key);
236 }
237 catch (Throwable t) {
238 // Ignore
239 }
240
241 try {
242 putMethod.invoke(instance, new Object[]{key, value});
243 }
244 catch (Exception ex) {
245 throw new ClazzAccessException(
246 "Cannot set property : "
247 + property.getName()
248 + ": cannot invoke method: "
249 + putMethod.getName(),
250 ex);
251 }
252 return oldValue;
253 }
254 else {
255 Map map = getPropertyValue();
256 if (map == null) {
257 map = new HashMap();
258 setPropertyValue(map);
259 }
260 return map.put(key, value);
261 }
262 }
263
264 /**
265 * @see java.util.Map#remove(java.lang.Object)
266 */
267 public Object remove(Object key) {
268 Method removeMethod = property.getRemoveMethod();
269 if (removeMethod != null) {
270 Object oldValue = null;
271 try {
272 oldValue = get(key);
273 }
274 catch (Throwable t) {
275 // Ignore
276 }
277
278 try {
279 removeMethod.invoke(instance, new Object[]{key});
280 }
281 catch (Exception ex) {
282 throw new ClazzAccessException(
283 "Cannot set property : "
284 + property.getName()
285 + ": cannot invoke method: "
286 + removeMethod.getName(),
287 ex);
288 }
289 return oldValue;
290 }
291 else {
292 Map map = getPropertyValue();
293 if (map != null) {
294 return map.remove(key);
295 }
296 return null;
297 }
298 }
299
300 private RuntimeException accessException(
301 String message,
302 Method method,
303 Throwable ex)
304 {
305 if (ex instanceof InvocationTargetException) {
306 ex = ((InvocationTargetException) ex).getTargetException();
307 }
308
309 // Just re-throw all runtime exceptions - there is really no
310 // point in wrapping them
311 if (ex instanceof RuntimeException) {
312 throw (RuntimeException) ex;
313 }
314 if (ex instanceof Error) {
315 throw (Error) ex;
316 }
317
318 throw new ClazzAccessException(
319 message
320 + ": "
321 + property.getName()
322 + ": cannot invoke method: "
323 + method.getName(),
324 ex);
325 }
326
327 private static final int ENTRIES = 0;
328 private static final int KEYS = 1;
329
330 /**
331 * An implementation of Set that delegates object deletion to the
332 * encompassing ReflectedMap.
333 */
334 private class EntrySet extends AbstractSet {
335 private int type;
336 private int modCount = -1;
337 private Set keySet;
338 private int size;
339
340 public EntrySet(int type) {
341 this.type = type;
342 update();
343 }
344
345 public void refresh() {
346 // If EntrySet is out of sync with the
347 // parent ReflectedMap, update the cached key set and size
348 if (modCount != ReflectedMap.this.modCount) {
349 update();
350 }
351 }
352
353 public void update() {
354 // Make sure modCount of EntrySet is maintained in sync with
355 // that of the embracing List
356 modCount = ReflectedMap.this.modCount;
357
358 keySet = ReflectedMap.this.getPropertyKeySet();
359 size = keySet.size();
360 }
361
362 public int size() {
363 refresh();
364 return size;
365 }
366
367 public Iterator iterator() {
368 refresh();
369 return new EntryIterator(keySet, type);
370 }
371
372 public boolean remove(Object object) {
373 refresh();
374 Object key =
375 (type == KEYS ? object : ((Map.Entry) object).getKey());
376
377 boolean exists = true;
378 try {
379 exists = keySet.contains(key);
380 }
381 catch (Throwable t) {
382 // Ignore
383 }
384 if (exists) {
385 ReflectedMap.this.remove(key);
386 }
387 return exists;
388 }
389 }
390
391 /**
392 * An implementation of Iterator that delegates object deletion to the
393 * encompassing ReflectedMap.
394 */
395 private class EntryIterator implements Iterator {
396 private int type;
397 private Set keySet;
398 private Iterator keyIterator;
399 private Object lastReturned = UNINITIALIZED;
400
401 public EntryIterator(Set keySet, int type) {
402 this.type = type;
403 this.keySet = keySet;
404 this.keyIterator = keySet.iterator();
405 }
406
407 /**
408 * @see java.util.Iterator#hasNext()
409 */
410 public boolean hasNext() {
411 return keyIterator.hasNext();
412 }
413
414 /**
415 * @see java.util.Iterator#next()
416 */
417 public Object next() {
418 lastReturned = keyIterator.next();
419 if (type == KEYS) {
420 return lastReturned;
421 }
422 else {
423 return new Entry(lastReturned);
424 }
425 }
426
427 /**
428 * @see java.util.Iterator#remove()
429 */
430 public void remove() {
431 if (lastReturned == UNINITIALIZED) {
432 throw new IllegalStateException();
433 }
434 ensureConcurrentChangeSafety();
435 ReflectedMap.this.remove(lastReturned);
436 }
437
438 /**
439 * This is called when we are about to delete a key from the map.
440 * The method checks if the set of keys we are iterating over is in fact
441 * a copy of the set of keys of the original collection. If it is not,
442 * the method creates such copy and re-executes the iteration steps that
443 * have already been made. The assumption made here is that as long as
444 * set remains unchanged, an iteration will always present elements in
445 * the same order (which is not to say that the order is predicatble).
446 */
447 private void ensureConcurrentChangeSafety() {
448 if (!(keySet instanceof ConcurrentChangeSafeSet)) {
449 keySet = new ConcurrentChangeSafeSet(keySet);
450 keyIterator = keySet.iterator();
451 while (keyIterator.hasNext()) {
452 Object key = keyIterator.next();
453 if ((key == null && lastReturned == null)
454 || (key != null && key.equals(lastReturned))) {
455 return;
456 }
457 }
458 throw new IllegalStateException(
459 "The second iteration over the key set"
460 + " did not produce the same elements");
461 }
462 }
463 }
464
465 private static final Object UNINITIALIZED = new Object();
466
467 /**
468 * An implementation of Map.Entry that maintains a key and gets the value
469 * from the property, if needed.
470 */
471 private class Entry implements Map.Entry {
472 private Object key;
473
474 public Entry(Object key) {
475 this.key = key;
476 }
477
478 /**
479 * @see java.util.Map.Entry#getKey()
480 */
481 public Object getKey() {
482 return key;
483 }
484 /**
485 * @see java.util.Map.Entry#getValue()
486 */
487 public Object getValue() {
488 return ReflectedMap.this.get(key);
489 }
490
491 /**
492 * @see java.util.Map.Entry#setValue(java.lang.Object)
493 */
494 public Object setValue(Object value) {
495 return ReflectedMap.this.put(key, value);
496 }
497
498 public boolean equals(Object o) {
499 if (!(o instanceof Map.Entry)) {
500 return false;
501 }
502
503 Map.Entry e = (Map.Entry) o;
504 return (key == null ? e.getKey() == null : key.equals(e.getKey()));
505 }
506
507 public int hashCode() {
508 return key == null ? 0 : key.hashCode();
509 }
510
511 public String toString() {
512 return key + "=" + getValue();
513 }
514 }
515
516 /**
517 * This is a simple HashSet. We are only introducing the subclass as a
518 * marker of the fact that we created the set rather than getting it
519 * directly from the map value of the property.
520 */
521 private static class ConcurrentChangeSafeSet extends HashSet {
522 public ConcurrentChangeSafeSet(Collection collection) {
523 super(collection);
524 }
525 }
526 }