View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.config;
20  
21  import org.apache.commons.beanutils.BeanUtils;
22  import org.apache.commons.beanutils.PropertyUtils;
23  import org.apache.shiro.codec.Base64;
24  import org.apache.shiro.codec.Hex;
25  import org.apache.shiro.util.*;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import java.beans.PropertyDescriptor;
30  import java.util.*;
31  
32  
33  /**
34   * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
35   * map of "property values".  Typically these come from the Shiro INI configuration and are used
36   * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
37   * <p/>
38   * Recognizes {@link Factory} implementations and will call
39   * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
40   *
41   * @since 0.9
42   */
43  public class ReflectionBuilder {
44  
45      //TODO - complete JavaDoc
46  
47      private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
48  
49      private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
50      private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
51      private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
52      private static final char MAP_KEY_VALUE_DELIMITER = ':';
53      private static final String HEX_BEGIN_TOKEN = "0x";
54      private static final String NULL_VALUE_TOKEN = "null";
55      private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
56      private static final char STRING_VALUE_DELIMETER = '"';
57      private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
58      private static final char MAP_PROPERTY_END_TOKEN = ']';
59  
60      private Map<String, ?> objects;
61  
62      public ReflectionBuilder() {
63          this.objects = new LinkedHashMap<String, Object>();
64      }
65  
66      public ReflectionBuilder(Map<String, ?> defaults) {
67          this.objects = CollectionUtils.isEmpty(defaults) ? new LinkedHashMap<String, Object>() : defaults;
68      }
69  
70      public Map<String, ?> getObjects() {
71          return objects;
72      }
73  
74      public void setObjects(Map<String, ?> objects) {
75          this.objects = CollectionUtils.isEmpty(objects) ? new LinkedHashMap<String, Object>() : objects;
76      }
77  
78      public Object getBean(String id) {
79          return objects.get(id);
80      }
81  
82      @SuppressWarnings({"unchecked"})
83      public <T> T getBean(String id, Class<T> requiredType) {
84          if (requiredType == null) {
85              throw new NullPointerException("requiredType argument cannot be null.");
86          }
87          Object bean = getBean(id);
88          if (bean == null) {
89              return null;
90          }
91          if (!requiredType.isAssignableFrom(bean.getClass())) {
92              throw new IllegalStateException("Bean with id [" + id + "] is not of the required type [" +
93                      requiredType.getName() + "].");
94          }
95          return (T) bean;
96      }
97  
98      @SuppressWarnings({"unchecked"})
99      public Map<String, ?> buildObjects(Map<String, String> kvPairs) {
100         if (kvPairs != null && !kvPairs.isEmpty()) {
101 
102             // Separate key value pairs into object declarations and property assignment
103             // so that all objects can be created up front
104 
105             //https://issues.apache.org/jira/browse/SHIRO-85 - need to use LinkedHashMaps here:
106             Map<String, String> instanceMap = new LinkedHashMap<String, String>();
107             Map<String, String> propertyMap = new LinkedHashMap<String, String>();
108 
109             for (Map.Entry<String, String> entry : kvPairs.entrySet()) {
110                 if (entry.getKey().indexOf('.') < 0 || entry.getKey().endsWith(".class")) {
111                     instanceMap.put(entry.getKey(), entry.getValue());
112                 } else {
113                     propertyMap.put(entry.getKey(), entry.getValue());
114                 }
115             }
116 
117             // Create all instances
118             for (Map.Entry<String, String> entry : instanceMap.entrySet()) {
119                 createNewInstance((Map<String, Object>) objects, entry.getKey(), entry.getValue());
120             }
121 
122             // Set all properties
123             for (Map.Entry<String, String> entry : propertyMap.entrySet()) {
124                 applyProperty(entry.getKey(), entry.getValue(), objects);
125             }
126         }
127 
128         //SHIRO-413: init method must be called for constructed objects that are Initializable
129         LifecycleUtils.init(objects.values());
130 
131         return objects;
132     }
133 
134     protected void createNewInstance(Map<String, Object> objects, String name, String value) {
135 
136         Object currentInstance = objects.get(name);
137         if (currentInstance != null) {
138             log.info("An instance with name '{}' already exists.  " +
139                     "Redefining this object as a new instance of type {}", name, value);
140         }
141 
142         Object instance;//name with no property, assume right hand side of equals sign is the class name:
143         try {
144             instance = ClassUtils.newInstance(value);
145             if (instance instanceof Nameable) {
146                 ((Nameable) instance).setName(name);
147             }
148         } catch (Exception e) {
149             String msg = "Unable to instantiate class [" + value + "] for object named '" + name + "'.  " +
150                     "Please ensure you've specified the fully qualified class name correctly.";
151             throw new ConfigurationException(msg, e);
152         }
153         objects.put(name, instance);
154     }
155 
156     protected void applyProperty(String key, String value, Map objects) {
157 
158         int index = key.indexOf('.');
159 
160         if (index >= 0) {
161             String name = key.substring(0, index);
162             String property = key.substring(index + 1, key.length());
163 
164             if (GLOBAL_PROPERTY_PREFIX.equalsIgnoreCase(name)) {
165                 applyGlobalProperty(objects, property, value);
166             } else {
167                 applySingleProperty(objects, name, property, value);
168             }
169 
170         } else {
171             throw new IllegalArgumentException("All property keys must contain a '.' character. " +
172                     "(e.g. myBean.property = value)  These should already be separated out by buildObjects().");
173         }
174     }
175 
176     protected void applyGlobalProperty(Map objects, String property, String value) {
177         for (Object instance : objects.values()) {
178             try {
179                 PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(instance, property);
180                 if (pd != null) {
181                     applyProperty(instance, property, value);
182                 }
183             } catch (Exception e) {
184                 String msg = "Error retrieving property descriptor for instance " +
185                         "of type [" + instance.getClass().getName() + "] " +
186                         "while setting property [" + property + "]";
187                 throw new ConfigurationException(msg, e);
188             }
189         }
190     }
191 
192     protected void applySingleProperty(Map objects, String name, String property, String value) {
193         Object instance = objects.get(name);
194         if (property.equals("class")) {
195             throw new IllegalArgumentException("Property keys should not contain 'class' properties since these " +
196                     "should already be separated out by buildObjects().");
197 
198         } else if (instance == null) {
199             String msg = "Configuration error.  Specified object [" + name + "] with property [" +
200                     property + "] without first defining that object's class.  Please first " +
201                     "specify the class property first, e.g. myObject = fully_qualified_class_name " +
202                     "and then define additional properties.";
203             throw new IllegalArgumentException(msg);
204 
205         } else {
206             applyProperty(instance, property, value);
207         }
208     }
209 
210     protected boolean isReference(String value) {
211         return value != null && value.startsWith(OBJECT_REFERENCE_BEGIN_TOKEN);
212     }
213 
214     protected String getId(String referenceToken) {
215         return referenceToken.substring(OBJECT_REFERENCE_BEGIN_TOKEN.length());
216     }
217 
218     protected Object getReferencedObject(String id) {
219         Object o = objects != null && !objects.isEmpty() ? objects.get(id) : null;
220         if (o == null) {
221             String msg = "The object with id [" + id + "] has not yet been defined and therefore cannot be " +
222                     "referenced.  Please ensure objects are defined in the order in which they should be " +
223                     "created and made available for future reference.";
224             throw new UnresolveableReferenceException(msg);
225         }
226         return o;
227     }
228 
229     protected String unescapeIfNecessary(String value) {
230         if (value != null && value.startsWith(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN)) {
231             return value.substring(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN.length() - 1);
232         }
233         return value;
234     }
235 
236     protected Object resolveReference(String reference) {
237         String id = getId(reference);
238         log.debug("Encountered object reference '{}'.  Looking up object with id '{}'", reference, id);
239         final Object referencedObject = getReferencedObject(id);
240         if (referencedObject instanceof Factory) {
241             return ((Factory) referencedObject).getInstance();
242         }
243         return referencedObject;
244     }
245 
246     protected boolean isTypedProperty(Object object, String propertyName, Class clazz) {
247         if (clazz == null) {
248             throw new NullPointerException("type (class) argument cannot be null.");
249         }
250         try {
251             PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, propertyName);
252             if (descriptor == null) {
253                 String msg = "Property '" + propertyName + "' does not exist for object of " +
254                         "type " + object.getClass().getName() + ".";
255                 throw new ConfigurationException(msg);
256             }
257             Class propertyClazz = descriptor.getPropertyType();
258             return clazz.isAssignableFrom(propertyClazz);
259         } catch (ConfigurationException ce) {
260             //let it propagate:
261             throw ce;
262         } catch (Exception e) {
263             String msg = "Unable to determine if property [" + propertyName + "] represents a " + clazz.getName();
264             throw new ConfigurationException(msg, e);
265         }
266     }
267 
268     protected Set<?> toSet(String sValue) {
269         String[] tokens = StringUtils.split(sValue);
270         if (tokens == null || tokens.length <= 0) {
271             return null;
272         }
273 
274         //SHIRO-423: check to see if the value is a referenced Set already, and if so, return it immediately:
275         if (tokens.length == 1 && isReference(tokens[0])) {
276             Object reference = resolveReference(tokens[0]);
277             if (reference instanceof Set) {
278                 return (Set)reference;
279             }
280         }
281 
282         Set<String> setTokens = new LinkedHashSet<String>(Arrays.asList(tokens));
283 
284         //now convert into correct values and/or references:
285         Set<Object> values = new LinkedHashSet<Object>(setTokens.size());
286         for (String token : setTokens) {
287             Object value = resolveValue(token);
288             values.add(value);
289         }
290         return values;
291     }
292 
293     protected Map<?, ?> toMap(String sValue) {
294         String[] tokens = StringUtils.split(sValue, StringUtils.DEFAULT_DELIMITER_CHAR,
295                 StringUtils.DEFAULT_QUOTE_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, true, true);
296         if (tokens == null || tokens.length <= 0) {
297             return null;
298         }
299 
300         //SHIRO-423: check to see if the value is a referenced Map already, and if so, return it immediately:
301         if (tokens.length == 1 && isReference(tokens[0])) {
302             Object reference = resolveReference(tokens[0]);
303             if (reference instanceof Map) {
304                 return (Map)reference;
305             }
306         }
307 
308         Map<String, String> mapTokens = new LinkedHashMap<String, String>(tokens.length);
309         for (String token : tokens) {
310             String[] kvPair = StringUtils.split(token, MAP_KEY_VALUE_DELIMITER);
311             if (kvPair == null || kvPair.length != 2) {
312                 String msg = "Map property value [" + sValue + "] contained key-value pair token [" +
313                         token + "] that does not properly split to a single key and pair.  This must be the " +
314                         "case for all map entries.";
315                 throw new ConfigurationException(msg);
316             }
317             mapTokens.put(kvPair[0], kvPair[1]);
318         }
319 
320         //now convert into correct values and/or references:
321         Map<Object, Object> map = new LinkedHashMap<Object, Object>(mapTokens.size());
322         for (Map.Entry<String, String> entry : mapTokens.entrySet()) {
323             Object key = resolveValue(entry.getKey());
324             Object value = resolveValue(entry.getValue());
325             map.put(key, value);
326         }
327         return map;
328     }
329 
330     // @since 1.2.2
331     // TODO: make protected in 1.3+
332     private Collection<?> toCollection(String sValue) {
333 
334         String[] tokens = StringUtils.split(sValue);
335         if (tokens == null || tokens.length <= 0) {
336             return null;
337         }
338 
339         //SHIRO-423: check to see if the value is a referenced Collection already, and if so, return it immediately:
340         if (tokens.length == 1 && isReference(tokens[0])) {
341             Object reference = resolveReference(tokens[0]);
342             if (reference instanceof Collection) {
343                 return (Collection)reference;
344             }
345         }
346 
347         //now convert into correct values and/or references:
348         List<Object> values = new ArrayList<Object>(tokens.length);
349         for (String token : tokens) {
350             Object value = resolveValue(token);
351             values.add(value);
352         }
353         return values;
354     }
355 
356     protected List<?> toList(String sValue) {
357         String[] tokens = StringUtils.split(sValue);
358         if (tokens == null || tokens.length <= 0) {
359             return null;
360         }
361 
362         //SHIRO-423: check to see if the value is a referenced List already, and if so, return it immediately:
363         if (tokens.length == 1 && isReference(tokens[0])) {
364             Object reference = resolveReference(tokens[0]);
365             if (reference instanceof List) {
366                 return (List)reference;
367             }
368         }
369 
370         //now convert into correct values and/or references:
371         List<Object> values = new ArrayList<Object>(tokens.length);
372         for (String token : tokens) {
373             Object value = resolveValue(token);
374             values.add(value);
375         }
376         return values;
377     }
378 
379     protected byte[] toBytes(String sValue) {
380         if (sValue == null) {
381             return null;
382         }
383         byte[] bytes;
384         if (sValue.startsWith(HEX_BEGIN_TOKEN)) {
385             String hex = sValue.substring(HEX_BEGIN_TOKEN.length());
386             bytes = Hex.decode(hex);
387         } else {
388             //assume base64 encoded:
389             bytes = Base64.decode(sValue);
390         }
391         return bytes;
392     }
393 
394     protected Object resolveValue(String stringValue) {
395         Object value;
396         if (isReference(stringValue)) {
397             value = resolveReference(stringValue);
398         } else {
399             value = unescapeIfNecessary(stringValue);
400         }
401         return value;
402     }
403 
404     protected String checkForNullOrEmptyLiteral(String stringValue) {
405         if (stringValue == null) {
406             return null;
407         }
408         //check if the value is the actual literal string 'null' (expected to be wrapped in quotes):
409         if (stringValue.equals("\"null\"")) {
410             return NULL_VALUE_TOKEN;
411         }
412         //or the actual literal string of two quotes '""' (expected to be wrapped in quotes):
413         else if (stringValue.equals("\"\"\"\"")) {
414             return EMPTY_STRING_VALUE_TOKEN;
415         } else {
416             return stringValue;
417         }
418     }
419     
420     protected void applyProperty(Object object, String propertyPath, Object value) {
421 
422         int mapBegin = propertyPath.indexOf(MAP_PROPERTY_BEGIN_TOKEN);
423         int mapEnd = -1;
424         String mapPropertyPath = null;
425         String keyString = null;
426 
427         String remaining = null;
428         
429         if (mapBegin >= 0) {
430             //a map is being referenced in the overall property path.  Find just the map's path:
431             mapPropertyPath = propertyPath.substring(0, mapBegin);
432             //find the end of the map reference:
433             mapEnd = propertyPath.indexOf(MAP_PROPERTY_END_TOKEN, mapBegin);
434             //find the token in between the [ and the ] (the map/array key or index):
435             keyString = propertyPath.substring(mapBegin+1, mapEnd);
436 
437             //find out if there is more path reference to follow.  If not, we're at a terminal of the OGNL expression
438             if (propertyPath.length() > (mapEnd+1)) {
439                 remaining = propertyPath.substring(mapEnd+1);
440                 if (remaining.startsWith(".")) {
441                     remaining = StringUtils.clean(remaining.substring(1));
442                 }
443             }
444         }
445         
446         if (remaining == null) {
447             //we've terminated the OGNL expression.  Check to see if we're assigning a property or a map entry:
448             if (keyString == null) {
449                 //not a map or array value assignment - assign the property directly:
450                 setProperty(object, propertyPath, value);
451             } else {
452                 //we're assigning a map or array entry.  Check to see which we should call:
453                 if (isTypedProperty(object, mapPropertyPath, Map.class)) {
454                     Map map = (Map)getProperty(object, mapPropertyPath);
455                     Object mapKey = resolveValue(keyString);
456                     //noinspection unchecked
457                     map.put(mapKey, value);
458                 } else {
459                     //must be an array property.  Convert the key string to an index:
460                     int index = Integer.valueOf(keyString);
461                     setIndexedProperty(object, mapPropertyPath, index, value);
462                 }
463             }
464         } else {
465             //property is being referenced as part of a nested path.  Find the referenced map/array entry and
466             //recursively call this method with the remaining property path
467             Object referencedValue = null;
468             if (isTypedProperty(object, mapPropertyPath, Map.class)) {
469                 Map map = (Map)getProperty(object, mapPropertyPath);
470                 Object mapKey = resolveValue(keyString);
471                 referencedValue = map.get(mapKey);
472             } else {
473                 //must be an array property:
474                 int index = Integer.valueOf(keyString);
475                 referencedValue = getIndexedProperty(object, mapPropertyPath, index);
476             }
477 
478             if (referencedValue == null) {
479                 throw new ConfigurationException("Referenced map/array value '" + mapPropertyPath + "[" +
480                 keyString + "]' does not exist.");
481             }
482 
483             applyProperty(referencedValue, remaining, value);
484         }
485     }
486     
487     private void setProperty(Object object, String propertyPath, Object value) {
488         try {
489             if (log.isTraceEnabled()) {
490                 log.trace("Applying property [{}] value [{}] on object of type [{}]",
491                         new Object[]{propertyPath, value, object.getClass().getName()});
492             }
493             BeanUtils.setProperty(object, propertyPath, value);
494         } catch (Exception e) {
495             String msg = "Unable to set property '" + propertyPath + "' with value [" + value + "] on object " +
496                     "of type " + (object != null ? object.getClass().getName() : null) + ".  If " +
497                     "'" + value + "' is a reference to another (previously defined) object, prefix it with " +
498                     "'" + OBJECT_REFERENCE_BEGIN_TOKEN + "' to indicate that the referenced " +
499                     "object should be used as the actual value.  " +
500                     "For example, " + OBJECT_REFERENCE_BEGIN_TOKEN + value;
501             throw new ConfigurationException(msg, e);
502         }
503     }
504     
505     private Object getProperty(Object object, String propertyPath) {
506         try {
507             return PropertyUtils.getProperty(object, propertyPath);
508         } catch (Exception e) {
509             throw new ConfigurationException("Unable to access property '" + propertyPath + "'", e);
510         }
511     }
512     
513     private void setIndexedProperty(Object object, String propertyPath, int index, Object value) {
514         try {
515             PropertyUtils.setIndexedProperty(object, propertyPath, index, value);
516         } catch (Exception e) {
517             throw new ConfigurationException("Unable to set array property '" + propertyPath + "'", e);
518         }
519     }
520     
521     private Object getIndexedProperty(Object object, String propertyPath, int index) {
522         try {
523             return PropertyUtils.getIndexedProperty(object, propertyPath, index);
524         } catch (Exception e) {
525             throw new ConfigurationException("Unable to acquire array property '" + propertyPath + "'", e);
526         }
527     }
528     
529     protected boolean isIndexedPropertyAssignment(String propertyPath) {
530         return propertyPath.endsWith("" + MAP_PROPERTY_END_TOKEN);
531     }
532 
533     protected void applyProperty(Object object, String propertyName, String stringValue) {
534 
535         Object value;
536 
537         if (NULL_VALUE_TOKEN.equals(stringValue)) {
538             value = null;
539         } else if (EMPTY_STRING_VALUE_TOKEN.equals(stringValue)) {
540             value = StringUtils.EMPTY_STRING;
541         } else if (isIndexedPropertyAssignment(propertyName)) {
542             String checked = checkForNullOrEmptyLiteral(stringValue);
543             value = resolveValue(checked);
544         } else if (isTypedProperty(object, propertyName, Set.class)) {
545             value = toSet(stringValue);
546         } else if (isTypedProperty(object, propertyName, Map.class)) {
547             value = toMap(stringValue);
548         } else if (isTypedProperty(object, propertyName, List.class)) {
549             value = toList(stringValue);
550         } else if (isTypedProperty(object, propertyName, Collection.class)) {
551             value = toCollection(stringValue);
552         } else if (isTypedProperty(object, propertyName, byte[].class)) {
553             value = toBytes(stringValue);
554         } else if (isTypedProperty(object, propertyName, ByteSource.class)) {
555             byte[] bytes = toBytes(stringValue);
556             value = ByteSource.Util.bytes(bytes);
557         } else {
558             String checked = checkForNullOrEmptyLiteral(stringValue);
559             value = resolveValue(checked);
560         }
561 
562         applyProperty(object, propertyName, value);
563     }
564 
565 }