View Javadoc
1   /*
2    * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
3    * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4    *
5    * This code is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU General Public License version 2 only, as
7    * published by the Free Software Foundation.  Oracle designates this
8    * particular file as subject to the "Classpath" exception as provided
9    * by Oracle in the LICENSE file that accompanied this code.
10   *
11   * This code is distributed in the hope that it will be useful, but WITHOUT
12   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13   * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14   * version 2 for more details (a copy is included in the LICENSE file that
15   * accompanied this code).
16   *
17   * You should have received a copy of the GNU General Public License version
18   * 2 along with this work; if not, write to the Free Software Foundation,
19   * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20   *
21   * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22   * or visit www.oracle.com if you need additional information or have any
23   * questions.
24   */
25  
26  package com.oracle.webservices.internal.api.message;
27  
28  import com.sun.istack.internal.NotNull;
29  import com.sun.istack.internal.Nullable;
30  
31  import java.lang.reflect.Field;
32  import java.lang.reflect.InvocationTargetException;
33  import java.lang.reflect.Method;
34  import java.security.AccessController;
35  import java.security.PrivilegedAction;
36  import java.util.AbstractMap;
37  import java.util.HashMap;
38  import java.util.HashSet;
39  import java.util.Map;
40  import java.util.Map.Entry;
41  import java.util.Set;
42  
43  
44  /**
45   * A set of "properties" that can be accessed via strongly-typed fields
46   * as well as reflexibly through the property name.
47   *
48   * @author Kohsuke Kawaguchi
49   */
50  @SuppressWarnings("SuspiciousMethodCalls")
51  public abstract class BasePropertySet implements PropertySet {
52  
53      /**
54       * Creates a new instance of TypedMap.
55       */
56      protected BasePropertySet() {
57      }
58  
59      private Map<String,Object> mapView;
60  
61      /**
62       * Represents the list of strongly-typed known properties
63       * (keyed by property names.)
64       *
65       * <p>
66       * Just giving it an alias to make the use of this class more fool-proof.
67       */
68      protected static class PropertyMap extends HashMap<String,Accessor> {
69  
70          // the entries are often being iterated through so performance can be improved
71          // by their caching instead of iterating through the original (immutable) map each time
72          transient PropertyMapEntry[] cachedEntries = null;
73  
74          PropertyMapEntry[] getPropertyMapEntries() {
75              if (cachedEntries == null) {
76                  cachedEntries = createPropertyMapEntries();
77              }
78              return cachedEntries;
79          }
80  
81          private PropertyMapEntry[] createPropertyMapEntries() {
82              final PropertyMapEntry[] modelEntries = new PropertyMapEntry[size()];
83              int i = 0;
84              for (final Entry<String, Accessor> e : entrySet()) {
85                  modelEntries[i++] = new PropertyMapEntry(e.getKey(), e.getValue());
86              }
87              return modelEntries;
88          }
89  
90      }
91  
92      /**
93       * PropertyMapEntry represents a Map.Entry in the PropertyMap with more efficient access.
94       */
95      static public class PropertyMapEntry {
96          public PropertyMapEntry(String k, Accessor v) {
97              key = k; value = v;
98          }
99          String key;
100         Accessor value;
101     }
102 
103     /**
104      * Map representing the Fields and Methods annotated with {@link PropertySet.Property}.
105      * Model of {@link PropertySet} class.
106      *
107      * <p>
108      * At the end of the derivation chain this method just needs to be implemented
109      * as:
110      *
111      * <pre>
112      * private static final PropertyMap model;
113      * static {
114      *   model = parse(MyDerivedClass.class);
115      * }
116      * protected PropertyMap getPropertyMap() {
117      *   return model;
118      * }
119      * </pre>
120      */
121     protected abstract PropertyMap getPropertyMap();
122 
123     /**
124      * This method parses a class for fields and methods with {@link PropertySet.Property}.
125      */
126     protected static PropertyMap parse(final Class clazz) {
127         // make all relevant fields and methods accessible.
128         // this allows runtime to skip the security check, so they runs faster.
129         return AccessController.doPrivileged(new PrivilegedAction<PropertyMap>() {
130             @Override
131             public PropertyMap run() {
132                 PropertyMap props = new PropertyMap();
133                 for (Class c=clazz; c!=null; c=c.getSuperclass()) {
134                     for (Field f : c.getDeclaredFields()) {
135                         Property cp = f.getAnnotation(Property.class);
136                         if(cp!=null) {
137                             for(String value : cp.value()) {
138                                 props.put(value, new FieldAccessor(f, value));
139                             }
140                         }
141                     }
142                     for (Method m : c.getDeclaredMethods()) {
143                         Property cp = m.getAnnotation(Property.class);
144                         if(cp!=null) {
145                             String name = m.getName();
146                             assert name.startsWith("get") || name.startsWith("is");
147 
148                             String setName = name.startsWith("is") ? "set"+name.substring(2) : // isFoo -> setFoo
149                                                                      's'  +name.substring(1);  // getFoo -> setFoo
150                             Method setter;
151                             try {
152                                 setter = clazz.getMethod(setName,m.getReturnType());
153                             } catch (NoSuchMethodException e) {
154                                 setter = null; // no setter
155                             }
156                             for(String value : cp.value()) {
157                                 props.put(value, new MethodAccessor(m, setter, value));
158                             }
159                         }
160                     }
161                 }
162 
163                 return props;
164             }
165         });
166     }
167 
168     /**
169      * Represents a typed property defined on a {@link PropertySet}.
170      */
171     protected interface Accessor {
172         String getName();
173         boolean hasValue(PropertySet props);
174         Object get(PropertySet props);
175         void set(PropertySet props, Object value);
176     }
177 
178     static final class FieldAccessor implements Accessor {
179         /**
180          * Field with the annotation.
181          */
182         private final Field f;
183 
184         /**
185          * One of the values in {@link Property} annotation on {@link #f}.
186          */
187         private final String name;
188 
189         protected FieldAccessor(Field f, String name) {
190             this.f = f;
191             f.setAccessible(true);
192             this.name = name;
193         }
194 
195         @Override
196         public String getName() {
197             return name;
198         }
199 
200         @Override
201         public boolean hasValue(PropertySet props) {
202             return get(props)!=null;
203         }
204 
205         @Override
206         public Object get(PropertySet props) {
207             try {
208                 return f.get(props);
209             } catch (IllegalAccessException e) {
210                 throw new AssertionError();
211             }
212         }
213 
214         @Override
215         public void set(PropertySet props, Object value) {
216             try {
217                 f.set(props,value);
218             } catch (IllegalAccessException e) {
219                 throw new AssertionError();
220             }
221         }
222     }
223 
224     static final class MethodAccessor implements Accessor {
225         /**
226          * Getter method.
227          */
228         private final @NotNull Method getter;
229         /**
230          * Setter method.
231          * Some property is read-only.
232          */
233         private final @Nullable Method setter;
234 
235         /**
236          * One of the values in {@link Property} annotation on {@link #getter}.
237          */
238         private final String name;
239 
240         protected MethodAccessor(Method getter, Method setter, String value) {
241             this.getter = getter;
242             this.setter = setter;
243             this.name = value;
244             getter.setAccessible(true);
245             if (setter!=null) {
246                 setter.setAccessible(true);
247             }
248         }
249 
250         @Override
251         public String getName() {
252             return name;
253         }
254 
255         @Override
256         public boolean hasValue(PropertySet props) {
257             return get(props)!=null;
258         }
259 
260         @Override
261         public Object get(PropertySet props) {
262             try {
263                 return getter.invoke(props);
264             } catch (IllegalAccessException e) {
265                 throw new AssertionError();
266             } catch (InvocationTargetException e) {
267                 handle(e);
268                 return 0;   // never reach here
269             }
270         }
271 
272         @Override
273         public void set(PropertySet props, Object value) {
274             if(setter==null) {
275                 throw new ReadOnlyPropertyException(getName());
276             }
277             try {
278                 setter.invoke(props,value);
279             } catch (IllegalAccessException e) {
280                 throw new AssertionError();
281             } catch (InvocationTargetException e) {
282                 handle(e);
283             }
284         }
285 
286         /**
287          * Since we don't expect the getter/setter to throw a checked exception,
288          * it should be possible to make the exception propagation transparent.
289          * That's what we are trying to do here.
290          */
291         private Exception handle(InvocationTargetException e) {
292             Throwable t = e.getTargetException();
293             if (t instanceof Error) {
294                 throw (Error)t;
295             }
296             if (t instanceof RuntimeException) {
297                 throw (RuntimeException)t;
298             }
299             throw new Error(e);
300         }
301     }
302 
303 
304     /**
305      * Class allowing to work with PropertySet object as with a Map; it doesn't only allow to read properties from
306      * the map but also to modify the map in a way it is in sync with original strongly typed fields. It also allows
307      * (if necessary) to store additional properties those can't be found in strongly typed fields.
308      *
309      * @see com.sun.xml.internal.ws.api.PropertySet#asMap() method
310      */
311     final class MapView extends HashMap<String, Object> {
312 
313         // flag if it should allow store also different properties
314         // than the from strongly typed fields
315         boolean extensible;
316 
317         MapView(boolean extensible) {
318                 super(getPropertyMap().getPropertyMapEntries().length);
319             this.extensible = extensible;
320             initialize();
321         }
322 
323         public void initialize() {
324             // iterate (cached) array instead of map to speed things up ...
325             PropertyMapEntry[] entries = getPropertyMap().getPropertyMapEntries();
326             for (PropertyMapEntry entry : entries) {
327                 super.put(entry.key, entry.value);
328             }
329         }
330 
331         @Override
332         public Object get(Object key) {
333             Object o = super.get(key);
334             if (o instanceof Accessor) {
335                 return ((Accessor) o).get(BasePropertySet.this);
336             } else {
337                 return o;
338             }
339         }
340 
341         @Override
342         public Set<Entry<String, Object>> entrySet() {
343             Set<Entry<String, Object>> entries = new HashSet<Entry<String, Object>>();
344             for (String key : keySet()) {
345                 entries.add(new SimpleImmutableEntry<String, Object>(key, get(key)));
346             }
347             return entries;
348         }
349 
350         @Override
351         public Object put(String key, Object value) {
352 
353             Object o = super.get(key);
354             if (o != null && o instanceof Accessor) {
355 
356                 Object oldValue = ((Accessor) o).get(BasePropertySet.this);
357                 ((Accessor) o).set(BasePropertySet.this, value);
358                 return oldValue;
359 
360             } else {
361 
362                 if (extensible) {
363                     return super.put(key, value);
364                 } else {
365                     throw new IllegalStateException("Unknown property [" + key + "] for PropertySet [" +
366                             BasePropertySet.this.getClass().getName() + "]");
367                 }
368             }
369         }
370 
371         @Override
372         public void clear() {
373             for (String key : keySet()) {
374                 remove(key);
375             }
376         }
377 
378         @Override
379         public Object remove(Object key) {
380             Object o;
381             o = super.get(key);
382             if (o instanceof Accessor) {
383                 ((Accessor)o).set(BasePropertySet.this, null);
384             }
385             return super.remove(key);
386         }
387     }
388 
389     @Override
390     public boolean containsKey(Object key) {
391         Accessor sp = getPropertyMap().get(key);
392         if (sp != null) {
393             return sp.get(this) != null;
394         }
395         return false;
396     }
397 
398     /**
399      * Gets the name of the property.
400      *
401      * @param key
402      *      This field is typed as {@link Object} to follow the {@link Map#get(Object)}
403      *      convention, but if anything but {@link String} is passed, this method
404      *      just returns null.
405      */
406     @Override
407     public Object get(Object key) {
408         Accessor sp = getPropertyMap().get(key);
409         if (sp != null) {
410             return sp.get(this);
411         }
412         throw new IllegalArgumentException("Undefined property "+key);
413     }
414 
415     /**
416      * Sets a property.
417      *
418      * <h3>Implementation Note</h3>
419      * This method is slow. Code inside JAX-WS should define strongly-typed
420      * fields in this class and access them directly, instead of using this.
421      *
422      * @throws ReadOnlyPropertyException
423      *      if the given key is an alias of a strongly-typed field,
424      *      and if the name object given is not assignable to the field.
425      *
426      * @see Property
427      */
428     @Override
429     public Object put(String key, Object value) {
430         Accessor sp = getPropertyMap().get(key);
431         if(sp!=null) {
432             Object old = sp.get(this);
433             sp.set(this,value);
434             return old;
435         } else {
436             throw new IllegalArgumentException("Undefined property "+key);
437         }
438     }
439 
440     /**
441      * Checks if this {@link PropertySet} supports a property of the given name.
442      */
443     @Override
444     public boolean supports(Object key) {
445         return getPropertyMap().containsKey(key);
446     }
447 
448     @Override
449     public Object remove(Object key) {
450         Accessor sp = getPropertyMap().get(key);
451         if(sp!=null) {
452             Object old = sp.get(this);
453             sp.set(this,null);
454             return old;
455         } else {
456             throw new IllegalArgumentException("Undefined property "+key);
457         }
458     }
459 
460     /**
461      * Creates a {@link Map} view of this {@link PropertySet}.
462      *
463      * <p>
464      * This map is partially live, in the sense that values you set to it
465      * will be reflected to {@link PropertySet}.
466      *
467      * <p>
468      * However, this map may not pick up changes made
469      * to {@link PropertySet} after the view is created.
470      *
471      * @deprecated use newer implementation {@link PropertySet#asMap()} which produces
472      * readwrite {@link Map}
473      *
474      * @return
475      *      always non-null valid instance.
476      */
477     @Deprecated
478     @Override
479     public final Map<String,Object> createMapView() {
480         final Set<Entry<String,Object>> core = new HashSet<Entry<String,Object>>();
481         createEntrySet(core);
482 
483         return new AbstractMap<String, Object>() {
484             @Override
485             public Set<Entry<String,Object>> entrySet() {
486                 return core;
487             }
488         };
489     }
490 
491     /**
492      * Creates a modifiable {@link Map} view of this {@link PropertySet}.
493      * <p/>
494      * Changes done on this {@link Map} or on {@link PropertySet} object work in both directions - values made to
495      * {@link Map} are reflected to {@link PropertySet} and changes done using getters/setters on {@link PropertySet}
496      * object are automatically reflected in this {@link Map}.
497      * <p/>
498      * If necessary, it also can hold other values (not present on {@link PropertySet}) -
499      * {@see PropertySet#mapAllowsAdditionalProperties}
500      *
501      * @return always non-null valid instance.
502      */
503     @Override
504     public Map<String, Object> asMap() {
505         if (mapView == null) {
506             mapView = createView();
507         }
508         return mapView;
509     }
510 
511     protected Map<String, Object> createView() {
512         return new MapView(mapAllowsAdditionalProperties());
513     }
514 
515     /**
516      * Used when constructing the {@link MapView} for this object - it controls if the {@link MapView} servers only to
517      * access strongly typed values or allows also different values
518      *
519      * @return true if {@link Map} should allow also properties not defined as strongly typed fields
520      */
521     protected boolean mapAllowsAdditionalProperties() {
522         return false;
523     }
524 
525     protected void createEntrySet(Set<Entry<String,Object>> core) {
526         for (final Entry<String, Accessor> e : getPropertyMap().entrySet()) {
527             core.add(new Entry<String, Object>() {
528                 @Override
529                 public String getKey() {
530                     return e.getKey();
531                 }
532 
533                 @Override
534                 public Object getValue() {
535                     return e.getValue().get(BasePropertySet.this);
536                 }
537 
538                 @Override
539                 public Object setValue(Object value) {
540                     Accessor acc = e.getValue();
541                     Object old = acc.get(BasePropertySet.this);
542                     acc.set(BasePropertySet.this,value);
543                     return old;
544                 }
545             });
546         }
547     }
548 }