001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.chain.impl;
018
019import java.beans.IntrospectionException;
020import java.beans.Introspector;
021import java.beans.PropertyDescriptor;
022import java.io.Serializable;
023import java.lang.reflect.Method;
024import java.util.AbstractCollection;
025import java.util.AbstractSet;
026import java.util.Collection;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.Map;
030import java.util.Objects;
031import java.util.Set;
032
033import org.apache.commons.chain.Context;
034
035/**
036 * Convenience base class for {@link Context} implementations.
037 *
038 * <p>In addition to the minimal functionality required by the {@link Context}
039 * interface, this class implements the recommended support for
040 * <em>Attribute-Property Transparency</em>. This is implemented by
041 * analyzing the available JavaBeans properties of this class (or its
042 * subclass), exposes them as key-value pairs in the {@code Map},
043 * with the key being the name of the property itself.</p>
044 *
045 * <p><strong>IMPLEMENTATION NOTE</strong> - Because {@code empty} is a
046 * read-only property defined by the {@code Map} interface, it may not
047 * be utilized as an attribute key or property name.</p>
048 *
049 * @author Craig R. McClanahan
050 * @version $Revision$ $Date$
051 */
052public class ContextBase extends HashMap<String, Object> implements Context {
053    private static final long serialVersionUID = -2482145117370708259L;
054
055    // ------------------------------------------------------ Static Variables
056
057    /**
058     * Distinguished singleton value that is stored in the map for each
059     * key that is actually a property. This value is used to ensure that
060     * {@code equals()} comparisons will always fail.
061     */
062    private static final Object SINGLETON = new Serializable() {
063        private static final long serialVersionUID = -6023767081282668587L;
064
065        @Override
066        public boolean equals(Object object) {
067            return false;
068        }
069
070        @Override
071        public int hashCode() {
072            return super.hashCode();
073        }
074    };
075
076    // ------------------------------------------------------ Instance Variables
077
078    // NOTE - PropertyDescriptor instances are not Serializable, so the
079    // following variables must be declared as transient. When a ContextBase
080    // instance is deserialized, the no-arguments constructor is called,
081    // and the initialize() method called there will repopulate them.
082    // Therefore, no special restoration activity is required.
083
084    /**
085     * The {@code PropertyDescriptor}s for all JavaBeans properties
086     * of this {@link Context} implementation class as an array.
087     */
088    private final transient PropertyDescriptor[] pds = getPropertyDescriptors();
089
090    /**
091     * The {@code PropertyDescriptor}s for all JavaBeans properties
092     * of this {@link Context} implementation class, keyed by property name.
093     * This collection is allocated only if there are any JavaBeans
094     * properties.
095     */
096    private final transient Map<String, PropertyDescriptor> descriptors = getMapDescriptors();
097
098    // ------------------------------------------------------------ Constructors
099
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}