1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.chain.impl;
18
19 import java.beans.IntrospectionException;
20 import java.beans.Introspector;
21 import java.beans.PropertyDescriptor;
22 import java.io.Serializable;
23 import java.lang.reflect.Method;
24 import java.util.AbstractCollection;
25 import java.util.AbstractSet;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.Iterator;
29 import java.util.Map;
30 import java.util.Objects;
31 import java.util.Set;
32
33 import org.apache.commons.chain.Context;
34
35 /**
36 * Convenience base class for {@link Context} implementations.
37 *
38 * <p>In addition to the minimal functionality required by the {@link Context}
39 * interface, this class implements the recommended support for
40 * <em>Attribute-Property Transparency</em>. This is implemented by
41 * analyzing the available JavaBeans properties of this class (or its
42 * subclass), exposes them as key-value pairs in the {@code Map},
43 * with the key being the name of the property itself.</p>
44 *
45 * <p><strong>IMPLEMENTATION NOTE</strong> - Because {@code empty} is a
46 * read-only property defined by the {@code Map} interface, it may not
47 * be utilized as an attribute key or property name.</p>
48 *
49 * @author Craig R. McClanahan
50 * @version $Revision$ $Date$
51 */
52 public class ContextBase extends HashMap<String, Object> implements Context {
53 private static final long serialVersionUID = -2482145117370708259L;
54
55 // ------------------------------------------------------ Static Variables
56
57 /**
58 * Distinguished singleton value that is stored in the map for each
59 * key that is actually a property. This value is used to ensure that
60 * {@code equals()} comparisons will always fail.
61 */
62 private static final Object SINGLETON = new Serializable() {
63 private static final long serialVersionUID = -6023767081282668587L;
64
65 @Override
66 public boolean equals(Object object) {
67 return false;
68 }
69
70 @Override
71 public int hashCode() {
72 return super.hashCode();
73 }
74 };
75
76 // ------------------------------------------------------ Instance Variables
77
78 // NOTE - PropertyDescriptor instances are not Serializable, so the
79 // following variables must be declared as transient. When a ContextBase
80 // instance is deserialized, the no-arguments constructor is called,
81 // and the initialize() method called there will repopulate them.
82 // Therefore, no special restoration activity is required.
83
84 /**
85 * The {@code PropertyDescriptor}s for all JavaBeans properties
86 * of this {@link Context} implementation class as an array.
87 */
88 private final transient PropertyDescriptor[] pds = getPropertyDescriptors();
89
90 /**
91 * The {@code PropertyDescriptor}s for all JavaBeans properties
92 * of this {@link Context} implementation class, keyed by property name.
93 * This collection is allocated only if there are any JavaBeans
94 * properties.
95 */
96 private final transient Map<String, PropertyDescriptor> descriptors = getMapDescriptors();
97
98 // ------------------------------------------------------------ Constructors
99
100 /**
101 * Default, no argument constructor.
102 */
103 public ContextBase() {
104 }
105
106 /**
107 * Initialize the contents of this {@link Context} by copying the
108 * values from the specified {@code Map}. Any keys in {@code map}
109 * that correspond to local properties will cause the setter method for
110 * that property to be called.
111 *
112 * @param map Map whose key-value pairs are added
113 *
114 * @throws IllegalArgumentException if an exception is thrown
115 * writing a local property value.
116 * @throws UnsupportedOperationException if a local property does not
117 * have a write method.
118 */
119 public ContextBase(Map<String, Object> map) {
120 super(map);
121 putAll(map);
122 }
123
124 // ------------------------------------------------------------- Map Methods
125
126 /**
127 * Override the default {@code Map} behavior to clear all keys and
128 * values except those corresponding to JavaBeans properties.
129 */
130 @Override
131 public void clear() {
132 if (descriptors == null) {
133 super.clear();
134 } else {
135 Iterator<String> keys = keySet().iterator();
136 while (keys.hasNext()) {
137 String key = keys.next();
138 if (!descriptors.containsKey(key)) {
139 keys.remove();
140 }
141 }
142 }
143 }
144
145 /**
146 * Override the default {@code Map} behavior to return
147 * {@code true} if the specified value is present in either the
148 * underlying {@code Map} or one of the local property values.
149 *
150 * @param value the value look for in the context.
151 *
152 * @return {@code true} if found in this context otherwise
153 * {@code false}.
154 *
155 * @throws IllegalArgumentException if a property getter
156 * throws an exception
157 */
158 @Override
159 public boolean containsValue(Object value) {
160 boolean b = super.containsValue(value);
161
162 // Case 1 -- no local properties
163 if (descriptors == null) {
164 return b;
165 }
166
167 // Case 2 -- value found in the underlying Map
168 if (b) {
169 return true;
170 }
171
172 // Case 3 -- check the values of our readable properties
173 for (PropertyDescriptor pd : pds) {
174 if (pd.getReadMethod() != null) {
175 Object prop = readProperty(pd);
176 if (value == null) {
177 if (prop == null) {
178 return true;
179 }
180 } else if (value.equals(prop)) {
181 return true;
182 }
183 }
184 }
185 return false;
186 }
187
188 /**
189 * Override the default {@code Map} behavior to return a
190 * {@code Set} that meets the specified default behavior except
191 * for attempts to remove the key for a property of the {@link Context}
192 * implementation class, which will throw
193 * {@code UnsupportedOperationException}.
194 *
195 * @return Set of entries in the Context.
196 */
197 @Override
198 public Set<Map.Entry<String, Object>> entrySet() {
199 return new EntrySetImpl();
200 }
201
202 /**
203 * Override the default {@code Map} behavior to return the value
204 * of a local property if the specified key matches a local property name.
205 *
206 * <p><strong>IMPLEMENTATION NOTE</strong> - If the specified
207 * {@code key} identifies a write-only property, {@code null}
208 * will arbitrarily be returned, in order to avoid difficulties implementing
209 * the contracts of the {@code Map} interface.</p>
210 *
211 * @param key Key of the value to be returned
212 *
213 * @return The value for the specified key.
214 *
215 * @throws IllegalArgumentException if an exception is thrown
216 * reading this local property value.
217 * @throws UnsupportedOperationException if this local property does not
218 * have a read method.
219 */
220 @Override
221 public Object get(Object key) {
222 // Case 1 -- no local properties
223 if (descriptors == null) {
224 return super.get(key);
225 }
226
227 // Case 2 -- this is a local property
228 if (key != null) {
229 final PropertyDescriptor descriptor = descriptors.get(key);
230 if (descriptor != null) {
231 if (descriptor.getReadMethod() != null) {
232 return readProperty(descriptor);
233 } else {
234 return null;
235 }
236 }
237 }
238
239 // Case 3 -- retrieve value from our underlying Map
240 return super.get(key);
241 }
242
243 /**
244 * Override the default {@code Map} behavior to return
245 * {@code true} if the underlying {@code Map} only contains
246 * key-value pairs for local properties (if any).
247 *
248 * @return {@code true} if this Context is empty, otherwise
249 * {@code false}.
250 */
251 @Override
252 public boolean isEmpty() {
253 // Case 1 -- no local properties
254 if (descriptors == null) {
255 return super.isEmpty();
256 }
257
258 // Case 2 -- compare key count to property count
259 return super.size() <= descriptors.size();
260 }
261
262 /**
263 * Override the default {@code Map} behavior to return a
264 * {@code Set} that meets the specified default behavior except
265 * for attempts to remove the key for a property of the {@link Context}
266 * implementation class, which will throw
267 * {@code UnsupportedOperationException}.
268 *
269 * @return The set of keys for objects in this Context.
270 */
271 public Set<String> keySet() {
272 return super.keySet();
273 }
274
275 /**
276 * Override the default {@code Map} behavior to set the value of a
277 * local property if the specified key matches a local property name.
278 *
279 * @param key Key of the value to be stored or replaced
280 * @param value New value to be stored
281 *
282 * @return The value added to the Context.
283 *
284 * @throws IllegalArgumentException if an exception is thrown
285 * reading or writing this local property value.
286 * @throws UnsupportedOperationException if this local property does not
287 * have both a read method and a write method
288 */
289 @Override
290 public Object put(String key, Object value) {
291 // Case 1 -- no local properties
292 if (descriptors == null) {
293 return super.put(key, value);
294 }
295
296 // Case 2 -- this is a local property
297 if (key != null) {
298 final PropertyDescriptor descriptor = descriptors.get(key);
299 if (descriptor != null) {
300 Object previous = null;
301 if (descriptor.getReadMethod() != null) {
302 previous = readProperty(descriptor);
303 }
304 writeProperty(descriptor, value);
305 return previous;
306 }
307 }
308
309 // Case 3 -- store or replace value in our underlying map
310 return super.put(key, value);
311 }
312
313 /**
314 * Override the default {@code Map} behavior to call the
315 * {@code put()} method individually for each key-value pair
316 * in the specified {@code Map}.
317 *
318 * @param map {@code Map} containing key-value pairs to store
319 * (or replace)
320 *
321 * @throws IllegalArgumentException if an exception is thrown
322 * reading or writing a local property value.
323 * @throws UnsupportedOperationException if a local property does not
324 * have both a read method and a write method
325 */
326 @Override
327 public void putAll(Map<? extends String, ? extends Object> map) {
328 map.forEach(this::put);
329 }
330
331 /**
332 * Override the default {@code Map} behavior to throw
333 * {@code UnsupportedOperationException} on any attempt to
334 * remove a key that is the name of a local property.
335 *
336 * @param key Key to be removed
337 *
338 * @return The value removed from the Context.
339 *
340 * @throws UnsupportedOperationException if the specified
341 * {@code key} matches the name of a local property
342 */
343 @Override
344 public Object remove(Object key) {
345 // Case 1 -- no local properties
346 if (descriptors == null) {
347 return super.remove(key);
348 }
349
350 // Case 2 -- this is a local property
351 if (key != null) {
352 PropertyDescriptor descriptor = descriptors.get(key);
353 if (descriptor != null) {
354 throw new UnsupportedOperationException("Local property '" + key + "' cannot be removed");
355 }
356 }
357
358 // Case 3 -- remove from underlying Map
359 return super.remove(key);
360 }
361
362 /**
363 * Override the default {@code Map} behavior to return a
364 * {@code Collection} that meets the specified default behavior except
365 * for attempts to remove the key for a property of the {@link Context}
366 * implementation class, which will throw
367 * {@code UnsupportedOperationException}.
368 *
369 * @return The collection of values in this Context.
370 */
371 @Override
372 public Collection<Object> values() {
373 return new ValuesImpl();
374 }
375
376 // --------------------------------------------------------- Private Methods
377
378 /**
379 * Return an {@code Iterator} over the set of {@code Map.Entry}
380 * objects representing our key-value pairs.
381 *
382 * @return an {@code Iterator} over the set of {@code Map.Entry} objects
383 */
384 private Iterator<Map.Entry<String, Object>> entriesIterator() {
385 return new EntrySetIterator();
386 }
387
388 /**
389 * Return a {@code Map.Entry} for the specified key value, if it
390 * is present; otherwise, return {@code null}.
391 *
392 * @param key Attribute key or property name
393 *
394 * @return a {@code Map.Entry} for the specified key value, if it
395 * is present; otherwise, return {@code null}
396 */
397 private Map.Entry<String, Object> entry(Object key) {
398 if (containsKey(key)) {
399 return new MapEntryImpl(key.toString(), get(key));
400 } else {
401 return null;
402 }
403 }
404
405 /**
406 * Get and return the value for the specified property.
407 *
408 * @param descriptor {@code PropertyDescriptor} for the
409 * specified property
410 *
411 * @return the value of the specified property
412 *
413 * @throws IllegalArgumentException if an exception is thrown
414 * reading this local property value.
415 * @throws UnsupportedOperationException if this local property does not
416 * have a read method.
417 */
418 private Object readProperty(PropertyDescriptor descriptor) {
419 try {
420 Method method = descriptor.getReadMethod();
421 if (method == null) {
422 throw new UnsupportedOperationException("Property '"
423 + descriptor.getName() + "' is not readable");
424 }
425 return method.invoke(this);
426 } catch (Exception e) {
427 throw new UnsupportedOperationException("Exception reading property '"
428 + descriptor.getName() + "': " + e.getMessage());
429 }
430 }
431
432 /**
433 * Remove the specified key-value pair, if it exists, and return
434 * {@code true}. If this pair does not exist, return {@code false}.
435 *
436 * @param entry Key-value pair to be removed
437 *
438 * @return if the specified key-value pair is removed, return
439 * {@code true}, otherwise {@code false}
440 *
441 * @throws UnsupportedOperationException if the specified key
442 * identifies a property instead of an attribute.
443 */
444 private boolean remove(Map.Entry<?, ?> entry) {
445 Map.Entry<String, Object> actual = entry(entry.getKey());
446 if (actual == null) {
447 return false;
448 } else if (entry.equals(actual)) {
449 remove(entry.getKey());
450 return true;
451 } else {
452 return false;
453 }
454 }
455
456 /**
457 * Return an {@code Iterator} over the set of values in this
458 * {@code Map}.
459 *
460 * @return an {@code Iterator} over the set of values in this
461 * {@code Map}
462 */
463 private Iterator<Object> valuesIterator() {
464 return new ValuesIterator();
465 }
466
467 /**
468 * Set the value for the specified property.
469 *
470 * @param descriptor {@code PropertyDescriptor} for the
471 * specified property
472 * @param value The new value for this property (must be of the
473 * correct type)
474 *
475 * @throws IllegalArgumentException if an exception is thrown
476 * writing this local property value.
477 * @throws UnsupportedOperationException if this local property does not
478 * have a write method.
479 */
480 private void writeProperty(PropertyDescriptor descriptor, Object value) {
481 try {
482 Method method = descriptor.getWriteMethod();
483 if (method == null) {
484 throw new UnsupportedOperationException("Property '" + descriptor.getName()
485 + "' is not writeable");
486 }
487 method.invoke(this, value);
488 } catch (Exception e) {
489 throw new UnsupportedOperationException("Exception writing property '"
490 + descriptor.getName() + "': " + e.getMessage());
491 }
492 }
493
494 /**
495 * Returns descriptors for all properties of the bean.
496 *
497 * @return descriptors for all properties of the bean
498 * or an empty array if an problem occurs
499 */
500 private PropertyDescriptor[] getPropertyDescriptors() {
501 // Retrieve the set of property descriptors for this Context class
502 try {
503 return Introspector.getBeanInfo(getClass()).getPropertyDescriptors();
504 } catch (IntrospectionException e) {
505 return new PropertyDescriptor[0]; // Should never happen
506 }
507 }
508
509 /**
510 * The {@code PropertyDescriptor}s for all JavaBeans properties
511 * of this {@link Context} implementation class, keyed by property
512 * name. This collection is allocated only if there are any JavaBeans
513 * properties.
514 *
515 * @return {@code PropertyDescriptor}s for all JavaBeans properties
516 * as an collection or {@code null} if there are no JavaBeans
517 * properties
518 */
519 private Map<String, PropertyDescriptor> getMapDescriptors() {
520 Map<String, PropertyDescriptor> ret = new HashMap<>();
521
522 // Initialize the underlying Map contents
523 for (PropertyDescriptor pd : pds) {
524 String name = pd.getName();
525
526 // Add descriptor (ignoring getClass() and isEmpty())
527 if (!("class".equals(name) || "empty".equals(name))) {
528 ret.put(name, pd);
529 super.put(name, SINGLETON);
530 }
531 }
532
533 return ret.isEmpty() ? null : ret;
534 }
535
536 // --------------------------------------------------------- Private Classes
537
538 /**
539 * Private implementation of {@code Set} that implements the
540 * semantics required for the value returned by {@code entrySet()}.
541 */
542 private final class EntrySetImpl extends AbstractSet<Map.Entry<String, Object>> {
543
544 @Override
545 public void clear() {
546 ContextBase.this.clear();
547 }
548
549 @Override
550 public boolean contains(Object obj) {
551 if (!(obj instanceof Map.Entry)) {
552 return false;
553 }
554 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
555 Map.Entry<String, Object> actual = ContextBase.this.entry(entry.getKey());
556 if (actual != null) {
557 return actual.equals(entry);
558 } else {
559 return false;
560 }
561 }
562
563 @Override
564 public boolean isEmpty() {
565 return ContextBase.this.isEmpty();
566 }
567
568 @Override
569 public Iterator<Map.Entry<String, Object>> iterator() {
570 return ContextBase.this.entriesIterator();
571 }
572
573 @Override
574 public boolean remove(Object obj) {
575 if (obj instanceof Map.Entry) {
576 return ContextBase.this.remove((Map.Entry<?, ?>) obj);
577 } else {
578 return false;
579 }
580 }
581
582 @Override
583 public int size() {
584 return ContextBase.this.size();
585 }
586 }
587
588 /**
589 * Private implementation of {@code Iterator} for the
590 * {@code Set} returned by {@code entrySet()}.
591 */
592 private final class EntrySetIterator implements Iterator<Map.Entry<String, Object>> {
593
594 private Map.Entry<String, Object> entry = null;
595 private Iterator<String> keys = ContextBase.this.keySet().iterator();
596
597 @Override
598 public boolean hasNext() {
599 return keys.hasNext();
600 }
601
602 @Override
603 public Map.Entry<String, Object> next() {
604 entry = ContextBase.this.entry(keys.next());
605 return entry;
606 }
607
608 @Override
609 public void remove() {
610 ContextBase.this.remove(entry);
611 }
612 }
613
614 /**
615 * Private implementation of {@code Map.Entry} for each item in
616 * {@code EntrySetImpl}.
617 */
618 private class MapEntryImpl implements Map.Entry<String, Object> {
619
620 private String key;
621 private Object value;
622
623 MapEntryImpl(String key, Object value) {
624 this.key = key;
625 this.value = value;
626 }
627
628 @Override
629 public boolean equals(Object obj) {
630 if (this == obj) {
631 return true;
632 }
633 if (obj == null || getClass() != obj.getClass()) {
634 return false;
635 }
636 MapEntryImpl other = (MapEntryImpl) obj;
637 if (!getEnclosingInstance().equals(other.getEnclosingInstance())) {
638 return false;
639 }
640 return Objects.equals(key, other.key) && Objects.equals(value, other.value);
641 }
642
643 @Override
644 public String getKey() {
645 return this.key;
646 }
647
648 @Override
649 public Object getValue() {
650 return this.value;
651 }
652
653 @Override
654 public int hashCode() {
655 return Objects.hashCode(key) ^ Objects.hashCode(value);
656 }
657
658 @Override
659 public Object setValue(Object value) {
660 Object previous = this.value;
661 ContextBase.this.put(this.key, value);
662 this.value = value;
663 return previous;
664 }
665
666 @Override
667 public String toString() {
668 return getKey() + "=" + getValue();
669 }
670
671 private ContextBase getEnclosingInstance() {
672 return ContextBase.this;
673 }
674 }
675
676 /**
677 * Private implementation of {@code Collection} that implements the
678 * semantics required for the value returned by {@code values()}.
679 */
680 private final class ValuesImpl extends AbstractCollection<Object> {
681
682 @Override
683 public void clear() {
684 ContextBase.this.clear();
685 }
686
687 @Override
688 public boolean contains(Object obj) {
689 if (!(obj instanceof Map.Entry)) {
690 return false;
691 }
692 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) obj;
693 return ContextBase.this.containsValue(entry.getValue());
694 }
695
696 @Override
697 public boolean isEmpty() {
698 return ContextBase.this.isEmpty();
699 }
700
701 @Override
702 public Iterator<Object> iterator() {
703 return ContextBase.this.valuesIterator();
704 }
705
706 @Override
707 public boolean remove(Object obj) {
708 if (obj instanceof Map.Entry) {
709 return ContextBase.this.remove((Map.Entry<?, ?>) obj);
710 } else {
711 return false;
712 }
713 }
714
715 @Override
716 public int size() {
717 return ContextBase.this.size();
718 }
719 }
720
721 /**
722 * Private implementation of {@code Iterator} for the
723 * {@code Collection} returned by {@code values()}.
724 */
725 private final class ValuesIterator implements Iterator<Object> {
726
727 private Map.Entry<String, Object> entry = null;
728 private Iterator<String> keys = ContextBase.this.keySet().iterator();
729
730 @Override
731 public boolean hasNext() {
732 return keys.hasNext();
733 }
734
735 @Override
736 public Object next() {
737 entry = ContextBase.this.entry(keys.next());
738 return entry.getValue();
739 }
740
741 @Override
742 public void remove() {
743 ContextBase.this.remove(entry);
744 }
745 }
746 }