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.config.event.BeanEvent;
26  import org.apache.shiro.config.event.ConfiguredBeanEvent;
27  import org.apache.shiro.config.event.DestroyedBeanEvent;
28  import org.apache.shiro.config.event.InitializedBeanEvent;
29  import org.apache.shiro.config.event.InstantiatedBeanEvent;
30  import org.apache.shiro.event.EventBus;
31  import org.apache.shiro.event.EventBusAware;
32  import org.apache.shiro.event.Subscribe;
33  import org.apache.shiro.event.support.DefaultEventBus;
34  import org.apache.shiro.util.Assert;
35  import org.apache.shiro.util.ByteSource;
36  import org.apache.shiro.util.ClassUtils;
37  import org.apache.shiro.util.Factory;
38  import org.apache.shiro.util.LifecycleUtils;
39  import org.apache.shiro.util.Nameable;
40  import org.apache.shiro.util.StringUtils;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import java.beans.PropertyDescriptor;
45  import java.util.ArrayList;
46  import java.util.Arrays;
47  import java.util.Collection;
48  import java.util.Collections;
49  import java.util.LinkedHashMap;
50  import java.util.LinkedHashSet;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Set;
54  
55  
56  /**
57   * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
58   * map of "property values".  Typically these come from the Shiro INI configuration and are used
59   * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
60   * <p/>
61   * Recognizes {@link Factory} implementations and will call
62   * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
63   *
64   * @since 0.9
65   */
66  public class ReflectionBuilder {
67  
68      //TODO - complete JavaDoc
69  
70      private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
71  
72      private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
73      private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
74      private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
75      private static final char MAP_KEY_VALUE_DELIMITER = ':';
76      private static final String HEX_BEGIN_TOKEN = "0x";
77      private static final String NULL_VALUE_TOKEN = "null";
78      private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
79      private static final char STRING_VALUE_DELIMETER = '"';
80      private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
81      private static final char MAP_PROPERTY_END_TOKEN = ']';
82  
83      private static final String EVENT_BUS_NAME = "eventBus";
84  
85      private final Map<String, Object> objects;
86  
87      /**
88       * Interpolation allows for ${key} substitution of values.
89       * @since 1.4
90       */
91      private Interpolator interpolator;
92  
93      /**
94       * @since 1.3
95       */
96      private EventBus eventBus;
97      /**
98       * Keeps track of event subscribers that were automatically registered by this ReflectionBuilder during
99       * object construction.  This is used in case a new EventBus is discovered during object graph
100      * construction:  upon discovery of the new EventBus, the existing subscribers will be unregistered from the
101      * old EventBus and then re-registered with the new EventBus.
102      *
103      * @since 1.3
104      */
105     private final Map<String,Object> registeredEventSubscribers;
106 
107     //@since 1.3
108     private Map<String,Object> createDefaultObjectMap() {
109         Map<String,Object> map = new LinkedHashMap<String, Object>();
110         map.put(EVENT_BUS_NAME, new DefaultEventBus());
111         return map;
112     }
113 
114     public ReflectionBuilder() {
115         this(null);
116     }
117 
118     public ReflectionBuilder(Map<String, ?> defaults) {
119 
120         this.interpolator = createInterpolator();
121 
122         this.objects = createDefaultObjectMap();
123         this.registeredEventSubscribers = new LinkedHashMap<String,Object>();
124         apply(defaults);
125     }
126 
127     private void apply(Map<String, ?> objects) {
128         if(!isEmpty(objects)) {
129             this.objects.putAll(objects);
130         }
131         EventBus found = findEventBus(this.objects);
132         Assert.notNull(found, "An " + EventBus.class.getName() + " instance must be present in the object defaults");
133         enableEvents(found);
134     }
135 
136     public Map<String, ?> getObjects() {
137         return objects;
138     }
139 
140     /**
141      * @param objects
142      */
143     public void setObjects(Map<String, ?> objects) {
144         this.objects.clear();
145         this.objects.putAll(createDefaultObjectMap());
146         apply(objects);
147     }
148 
149     //@since 1.3
150     private void enableEvents(EventBus eventBus) {
151         Assert.notNull(eventBus, "EventBus argument cannot be null.");
152         //clean up old auto-registered subscribers:
153         for (Object subscriber : this.registeredEventSubscribers.values()) {
154             this.eventBus.unregister(subscriber);
155         }
156         this.registeredEventSubscribers.clear();
157 
158         this.eventBus = eventBus;
159 
160         for(Map.Entry<String,Object> entry : this.objects.entrySet()) {
161             enableEventsIfNecessary(entry.getValue(), entry.getKey());
162         }
163     }
164 
165     //@since 1.3
166     private void enableEventsIfNecessary(Object bean, String name) {
167         boolean applied = applyEventBusIfNecessary(bean);
168         if (!applied) {
169             //if the event bus is applied, and the bean wishes to be a subscriber as well (not just a publisher),
170             // we assume that the implementation registers itself with the event bus, i.e. eventBus.register(this);
171 
172             //if the event bus isn't applied, only then do we need to check to see if the bean is an event subscriber,
173             // and if so, register it on the event bus automatically since it has no ability to do so itself:
174             if (isEventSubscriber(bean, name)) {
175                 //found an event subscriber, so register them with the EventBus:
176                 this.eventBus.register(bean);
177                 this.registeredEventSubscribers.put(name, bean);
178             }
179         }
180     }
181 
182     //@since 1.3
183     private boolean isEventSubscriber(Object bean, String name) {
184         List annotatedMethods = ClassUtils.getAnnotatedMethods(bean.getClass(), Subscribe.class);
185         return !isEmpty(annotatedMethods);
186     }
187 
188     //@since 1.3
189     protected EventBus findEventBus(Map<String,?> objects) {
190 
191         if (isEmpty(objects)) {
192             return null;
193         }
194 
195         //prefer a named object first:
196         Object value = objects.get(EVENT_BUS_NAME);
197         if (value != null && value instanceof EventBus) {
198             return (EventBus)value;
199         }
200 
201         //couldn't find a named 'eventBus' EventBus object.  Try to find the first typed value we can:
202         for( Object v : objects.values()) {
203             if (v instanceof EventBus) {
204                 return (EventBus)v;
205             }
206         }
207 
208         return null;
209     }
210 
211     private boolean applyEventBusIfNecessary(Object value) {
212         if (value instanceof EventBusAware) {
213             ((EventBusAware)value).setEventBus(this.eventBus);
214             return true;
215         }
216         return false;
217     }
218 
219     public Object getBean(String id) {
220         return objects.get(id);
221     }
222 
223     @SuppressWarnings({"unchecked"})
224     public <T> T getBean(String id, Class<T> requiredType) {
225         if (requiredType == null) {
226             throw new NullPointerException("requiredType argument cannot be null.");
227         }
228         Object bean = getBean(id);
229         if (bean == null) {
230             return null;
231         }
232         Assert.state(requiredType.isAssignableFrom(bean.getClass()),
233                 "Bean with id [" + id + "] is not of the required type [" + requiredType.getName() + "].");
234         return (T) bean;
235     }
236 
237     private String parseBeanId(String lhs) {
238         Assert.notNull(lhs);
239         if (lhs.indexOf('.') < 0) {
240             return lhs;
241         }
242         String classSuffix = ".class";
243         int index = lhs.indexOf(classSuffix);
244         if (index >= 0) {
245             return lhs.substring(0, index);
246         }
247         return null;
248     }
249 
250     @SuppressWarnings({"unchecked"})
251     public Map<String, ?> buildObjects(Map<String, String> kvPairs) {
252 
253         if (kvPairs != null && !kvPairs.isEmpty()) {
254 
255             BeanConfigurationProcessor processor = new BeanConfigurationProcessor();
256 
257             for (Map.Entry<String, String> entry : kvPairs.entrySet()) {
258                 String lhs = entry.getKey();
259                 String rhs = interpolator.interpolate(entry.getValue());
260 
261                 String beanId = parseBeanId(lhs);
262                 if (beanId != null) { //a beanId could be parsed, so the line is a bean instance definition
263                     processor.add(new InstantiationStatement(beanId, rhs));
264                 } else { //the line must be a property configuration
265                     processor.add(new AssignmentStatement(lhs, rhs));
266                 }
267             }
268 
269             processor.execute();
270         }
271 
272         //SHIRO-413: init method must be called for constructed objects that are Initializable
273         LifecycleUtils.init(objects.values());
274 
275         return objects;
276     }
277 
278     public void destroy() {
279         final Map<String, Object> immutableObjects = Collections.unmodifiableMap(objects);
280 
281         //destroy objects in the opposite order they were initialized:
282         List<Map.Entry<String,?>> entries = new ArrayList<Map.Entry<String,?>>(objects.entrySet());
283         Collections.reverse(entries);
284 
285         for(Map.Entry<String, ?> entry: entries) {
286             String id = entry.getKey();
287             Object bean = entry.getValue();
288 
289             //don't destroy the eventbus until the end - we need it to still be 'alive' while publishing destroy events:
290             if (bean != this.eventBus) { //memory equality check (not .equals) on purpose
291                 LifecycleUtils.destroy(bean);
292                 BeanEvent event = new DestroyedBeanEvent(id, bean, immutableObjects);
293                 eventBus.publish(event);
294                 this.eventBus.unregister(bean); //bean is now destroyed - it should not receive any other events
295             }
296         }
297         //only now destroy the event bus:
298         LifecycleUtils.destroy(this.eventBus);
299     }
300 
301     protected void createNewInstance(Map<String, Object> objects, String name, String value) {
302 
303         Object currentInstance = objects.get(name);
304         if (currentInstance != null) {
305             log.info("An instance with name '{}' already exists.  " +
306                     "Redefining this object as a new instance of type {}", name, value);
307         }
308 
309         Object instance;//name with no property, assume right hand side of equals sign is the class name:
310         try {
311             instance = ClassUtils.newInstance(value);
312             if (instance instanceof Nameable) {
313                 ((Nameable) instance).setName(name);
314             }
315         } catch (Exception e) {
316             String msg = "Unable to instantiate class [" + value + "] for object named '" + name + "'.  " +
317                     "Please ensure you've specified the fully qualified class name correctly.";
318             throw new ConfigurationException(msg, e);
319         }
320         objects.put(name, instance);
321     }
322 
323     protected void applyProperty(String key, String value, Map objects) {
324 
325         int index = key.indexOf('.');
326 
327         if (index >= 0) {
328             String name = key.substring(0, index);
329             String property = key.substring(index + 1, key.length());
330 
331             if (GLOBAL_PROPERTY_PREFIX.equalsIgnoreCase(name)) {
332                 applyGlobalProperty(objects, property, value);
333             } else {
334                 applySingleProperty(objects, name, property, value);
335             }
336 
337         } else {
338             throw new IllegalArgumentException("All property keys must contain a '.' character. " +
339                     "(e.g. myBean.property = value)  These should already be separated out by buildObjects().");
340         }
341     }
342 
343     protected void applyGlobalProperty(Map objects, String property, String value) {
344         for (Object instance : objects.values()) {
345             try {
346                 PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(instance, property);
347                 if (pd != null) {
348                     applyProperty(instance, property, value);
349                 }
350             } catch (Exception e) {
351                 String msg = "Error retrieving property descriptor for instance " +
352                         "of type [" + instance.getClass().getName() + "] " +
353                         "while setting property [" + property + "]";
354                 throw new ConfigurationException(msg, e);
355             }
356         }
357     }
358 
359     protected void applySingleProperty(Map objects, String name, String property, String value) {
360         Object instance = objects.get(name);
361         if (property.equals("class")) {
362             throw new IllegalArgumentException("Property keys should not contain 'class' properties since these " +
363                     "should already be separated out by buildObjects().");
364 
365         } else if (instance == null) {
366             String msg = "Configuration error.  Specified object [" + name + "] with property [" +
367                     property + "] without first defining that object's class.  Please first " +
368                     "specify the class property first, e.g. myObject = fully_qualified_class_name " +
369                     "and then define additional properties.";
370             throw new IllegalArgumentException(msg);
371 
372         } else {
373             applyProperty(instance, property, value);
374         }
375     }
376 
377     protected boolean isReference(String value) {
378         return value != null && value.startsWith(OBJECT_REFERENCE_BEGIN_TOKEN);
379     }
380 
381     protected String getId(String referenceToken) {
382         return referenceToken.substring(OBJECT_REFERENCE_BEGIN_TOKEN.length());
383     }
384 
385     protected Object getReferencedObject(String id) {
386         Object o = objects != null && !objects.isEmpty() ? objects.get(id) : null;
387         if (o == null) {
388             String msg = "The object with id [" + id + "] has not yet been defined and therefore cannot be " +
389                     "referenced.  Please ensure objects are defined in the order in which they should be " +
390                     "created and made available for future reference.";
391             throw new UnresolveableReferenceException(msg);
392         }
393         return o;
394     }
395 
396     protected String unescapeIfNecessary(String value) {
397         if (value != null && value.startsWith(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN)) {
398             return value.substring(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN.length() - 1);
399         }
400         return value;
401     }
402 
403     protected Object resolveReference(String reference) {
404         String id = getId(reference);
405         log.debug("Encountered object reference '{}'.  Looking up object with id '{}'", reference, id);
406         final Object referencedObject = getReferencedObject(id);
407         if (referencedObject instanceof Factory) {
408             return ((Factory) referencedObject).getInstance();
409         }
410         return referencedObject;
411     }
412 
413     protected boolean isTypedProperty(Object object, String propertyName, Class clazz) {
414         if (clazz == null) {
415             throw new NullPointerException("type (class) argument cannot be null.");
416         }
417         try {
418             PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(object, propertyName);
419             if (descriptor == null) {
420                 String msg = "Property '" + propertyName + "' does not exist for object of " +
421                         "type " + object.getClass().getName() + ".";
422                 throw new ConfigurationException(msg);
423             }
424             Class propertyClazz = descriptor.getPropertyType();
425             return clazz.isAssignableFrom(propertyClazz);
426         } catch (ConfigurationException ce) {
427             //let it propagate:
428             throw ce;
429         } catch (Exception e) {
430             String msg = "Unable to determine if property [" + propertyName + "] represents a " + clazz.getName();
431             throw new ConfigurationException(msg, e);
432         }
433     }
434 
435     protected Set<?> toSet(String sValue) {
436         String[] tokens = StringUtils.split(sValue);
437         if (tokens == null || tokens.length <= 0) {
438             return null;
439         }
440 
441         //SHIRO-423: check to see if the value is a referenced Set already, and if so, return it immediately:
442         if (tokens.length == 1 && isReference(tokens[0])) {
443             Object reference = resolveReference(tokens[0]);
444             if (reference instanceof Set) {
445                 return (Set)reference;
446             }
447         }
448 
449         Set<String> setTokens = new LinkedHashSet<String>(Arrays.asList(tokens));
450 
451         //now convert into correct values and/or references:
452         Set<Object> values = new LinkedHashSet<Object>(setTokens.size());
453         for (String token : setTokens) {
454             Object value = resolveValue(token);
455             values.add(value);
456         }
457         return values;
458     }
459 
460     protected Map<?, ?> toMap(String sValue) {
461         String[] tokens = StringUtils.split(sValue, StringUtils.DEFAULT_DELIMITER_CHAR,
462                 StringUtils.DEFAULT_QUOTE_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, true, true);
463         if (tokens == null || tokens.length <= 0) {
464             return null;
465         }
466 
467         //SHIRO-423: check to see if the value is a referenced Map already, and if so, return it immediately:
468         if (tokens.length == 1 && isReference(tokens[0])) {
469             Object reference = resolveReference(tokens[0]);
470             if (reference instanceof Map) {
471                 return (Map)reference;
472             }
473         }
474 
475         Map<String, String> mapTokens = new LinkedHashMap<String, String>(tokens.length);
476         for (String token : tokens) {
477             String[] kvPair = StringUtils.split(token, MAP_KEY_VALUE_DELIMITER);
478             if (kvPair == null || kvPair.length != 2) {
479                 String msg = "Map property value [" + sValue + "] contained key-value pair token [" +
480                         token + "] that does not properly split to a single key and pair.  This must be the " +
481                         "case for all map entries.";
482                 throw new ConfigurationException(msg);
483             }
484             mapTokens.put(kvPair[0], kvPair[1]);
485         }
486 
487         //now convert into correct values and/or references:
488         Map<Object, Object> map = new LinkedHashMap<Object, Object>(mapTokens.size());
489         for (Map.Entry<String, String> entry : mapTokens.entrySet()) {
490             Object key = resolveValue(entry.getKey());
491             Object value = resolveValue(entry.getValue());
492             map.put(key, value);
493         }
494         return map;
495     }
496 
497     // @since 1.2.2
498     protected Collection<?> toCollection(String sValue) {
499 
500         String[] tokens = StringUtils.split(sValue);
501         if (tokens == null || tokens.length <= 0) {
502             return null;
503         }
504 
505         //SHIRO-423: check to see if the value is a referenced Collection already, and if so, return it immediately:
506         if (tokens.length == 1 && isReference(tokens[0])) {
507             Object reference = resolveReference(tokens[0]);
508             if (reference instanceof Collection) {
509                 return (Collection)reference;
510             }
511         }
512 
513         //now convert into correct values and/or references:
514         List<Object> values = new ArrayList<Object>(tokens.length);
515         for (String token : tokens) {
516             Object value = resolveValue(token);
517             values.add(value);
518         }
519         return values;
520     }
521 
522     protected List<?> toList(String sValue) {
523         String[] tokens = StringUtils.split(sValue);
524         if (tokens == null || tokens.length <= 0) {
525             return null;
526         }
527 
528         //SHIRO-423: check to see if the value is a referenced List already, and if so, return it immediately:
529         if (tokens.length == 1 && isReference(tokens[0])) {
530             Object reference = resolveReference(tokens[0]);
531             if (reference instanceof List) {
532                 return (List)reference;
533             }
534         }
535 
536         //now convert into correct values and/or references:
537         List<Object> values = new ArrayList<Object>(tokens.length);
538         for (String token : tokens) {
539             Object value = resolveValue(token);
540             values.add(value);
541         }
542         return values;
543     }
544 
545     protected byte[] toBytes(String sValue) {
546         if (sValue == null) {
547             return null;
548         }
549         byte[] bytes;
550         if (sValue.startsWith(HEX_BEGIN_TOKEN)) {
551             String hex = sValue.substring(HEX_BEGIN_TOKEN.length());
552             bytes = Hex.decode(hex);
553         } else {
554             //assume base64 encoded:
555             bytes = Base64.decode(sValue);
556         }
557         return bytes;
558     }
559 
560     protected Object resolveValue(String stringValue) {
561         Object value;
562         if (isReference(stringValue)) {
563             value = resolveReference(stringValue);
564         } else {
565             value = unescapeIfNecessary(stringValue);
566         }
567         return value;
568     }
569 
570     protected String checkForNullOrEmptyLiteral(String stringValue) {
571         if (stringValue == null) {
572             return null;
573         }
574         //check if the value is the actual literal string 'null' (expected to be wrapped in quotes):
575         if (stringValue.equals("\"null\"")) {
576             return NULL_VALUE_TOKEN;
577         }
578         //or the actual literal string of two quotes '""' (expected to be wrapped in quotes):
579         else if (stringValue.equals("\"\"\"\"")) {
580             return EMPTY_STRING_VALUE_TOKEN;
581         } else {
582             return stringValue;
583         }
584     }
585     
586     protected void applyProperty(Object object, String propertyPath, Object value) {
587 
588         int mapBegin = propertyPath.indexOf(MAP_PROPERTY_BEGIN_TOKEN);
589         int mapEnd = -1;
590         String mapPropertyPath = null;
591         String keyString = null;
592 
593         String remaining = null;
594         
595         if (mapBegin >= 0) {
596             //a map is being referenced in the overall property path.  Find just the map's path:
597             mapPropertyPath = propertyPath.substring(0, mapBegin);
598             //find the end of the map reference:
599             mapEnd = propertyPath.indexOf(MAP_PROPERTY_END_TOKEN, mapBegin);
600             //find the token in between the [ and the ] (the map/array key or index):
601             keyString = propertyPath.substring(mapBegin+1, mapEnd);
602 
603             //find out if there is more path reference to follow.  If not, we're at a terminal of the OGNL expression
604             if (propertyPath.length() > (mapEnd+1)) {
605                 remaining = propertyPath.substring(mapEnd+1);
606                 if (remaining.startsWith(".")) {
607                     remaining = StringUtils.clean(remaining.substring(1));
608                 }
609             }
610         }
611         
612         if (remaining == null) {
613             //we've terminated the OGNL expression.  Check to see if we're assigning a property or a map entry:
614             if (keyString == null) {
615                 //not a map or array value assignment - assign the property directly:
616                 setProperty(object, propertyPath, value);
617             } else {
618                 //we're assigning a map or array entry.  Check to see which we should call:
619                 if (isTypedProperty(object, mapPropertyPath, Map.class)) {
620                     Map map = (Map)getProperty(object, mapPropertyPath);
621                     Object mapKey = resolveValue(keyString);
622                     //noinspection unchecked
623                     map.put(mapKey, value);
624                 } else {
625                     //must be an array property.  Convert the key string to an index:
626                     int index = Integer.valueOf(keyString);
627                     setIndexedProperty(object, mapPropertyPath, index, value);
628                 }
629             }
630         } else {
631             //property is being referenced as part of a nested path.  Find the referenced map/array entry and
632             //recursively call this method with the remaining property path
633             Object referencedValue = null;
634             if (isTypedProperty(object, mapPropertyPath, Map.class)) {
635                 Map map = (Map)getProperty(object, mapPropertyPath);
636                 Object mapKey = resolveValue(keyString);
637                 referencedValue = map.get(mapKey);
638             } else {
639                 //must be an array property:
640                 int index = Integer.valueOf(keyString);
641                 referencedValue = getIndexedProperty(object, mapPropertyPath, index);
642             }
643 
644             if (referencedValue == null) {
645                 throw new ConfigurationException("Referenced map/array value '" + mapPropertyPath + "[" +
646                 keyString + "]' does not exist.");
647             }
648 
649             applyProperty(referencedValue, remaining, value);
650         }
651     }
652     
653     private void setProperty(Object object, String propertyPath, Object value) {
654         try {
655             if (log.isTraceEnabled()) {
656                 log.trace("Applying property [{}] value [{}] on object of type [{}]",
657                         new Object[]{propertyPath, value, object.getClass().getName()});
658             }
659             BeanUtils.setProperty(object, propertyPath, value);
660         } catch (Exception e) {
661             String msg = "Unable to set property '" + propertyPath + "' with value [" + value + "] on object " +
662                     "of type " + (object != null ? object.getClass().getName() : null) + ".  If " +
663                     "'" + value + "' is a reference to another (previously defined) object, prefix it with " +
664                     "'" + OBJECT_REFERENCE_BEGIN_TOKEN + "' to indicate that the referenced " +
665                     "object should be used as the actual value.  " +
666                     "For example, " + OBJECT_REFERENCE_BEGIN_TOKEN + value;
667             throw new ConfigurationException(msg, e);
668         }
669     }
670     
671     private Object getProperty(Object object, String propertyPath) {
672         try {
673             return PropertyUtils.getProperty(object, propertyPath);
674         } catch (Exception e) {
675             throw new ConfigurationException("Unable to access property '" + propertyPath + "'", e);
676         }
677     }
678     
679     private void setIndexedProperty(Object object, String propertyPath, int index, Object value) {
680         try {
681             PropertyUtils.setIndexedProperty(object, propertyPath, index, value);
682         } catch (Exception e) {
683             throw new ConfigurationException("Unable to set array property '" + propertyPath + "'", e);
684         }
685     }
686     
687     private Object getIndexedProperty(Object object, String propertyPath, int index) {
688         try {
689             return PropertyUtils.getIndexedProperty(object, propertyPath, index);
690         } catch (Exception e) {
691             throw new ConfigurationException("Unable to acquire array property '" + propertyPath + "'", e);
692         }
693     }
694     
695     protected boolean isIndexedPropertyAssignment(String propertyPath) {
696         return propertyPath.endsWith("" + MAP_PROPERTY_END_TOKEN);
697     }
698 
699     protected void applyProperty(Object object, String propertyName, String stringValue) {
700 
701         Object value;
702 
703         if (NULL_VALUE_TOKEN.equals(stringValue)) {
704             value = null;
705         } else if (EMPTY_STRING_VALUE_TOKEN.equals(stringValue)) {
706             value = StringUtils.EMPTY_STRING;
707         } else if (isIndexedPropertyAssignment(propertyName)) {
708             String checked = checkForNullOrEmptyLiteral(stringValue);
709             value = resolveValue(checked);
710         } else if (isTypedProperty(object, propertyName, Set.class)) {
711             value = toSet(stringValue);
712         } else if (isTypedProperty(object, propertyName, Map.class)) {
713             value = toMap(stringValue);
714         } else if (isTypedProperty(object, propertyName, List.class)) {
715             value = toList(stringValue);
716         } else if (isTypedProperty(object, propertyName, Collection.class)) {
717             value = toCollection(stringValue);
718         } else if (isTypedProperty(object, propertyName, byte[].class)) {
719             value = toBytes(stringValue);
720         } else if (isTypedProperty(object, propertyName, ByteSource.class)) {
721             byte[] bytes = toBytes(stringValue);
722             value = ByteSource.Util.bytes(bytes);
723         } else {
724             String checked = checkForNullOrEmptyLiteral(stringValue);
725             value = resolveValue(checked);
726         }
727 
728         applyProperty(object, propertyName, value);
729     }
730 
731     private Interpolator createInterpolator() {
732 
733         if (ClassUtils.isAvailable("org.apache.commons.configuration2.interpol.ConfigurationInterpolator")) {
734             return new CommonsInterpolator();
735         }
736 
737         return new DefaultInterpolator();
738     }
739 
740     /**
741      * Sets the {@link Interpolator} used when evaluating the right side of the expressions.
742      * @since 1.4
743      */
744     public void setInterpolator(Interpolator interpolator) {
745         this.interpolator = interpolator;
746     }
747 
748     private class BeanConfigurationProcessor {
749 
750         private final List<Statement> statements = new ArrayList<Statement>();
751         private final List<BeanConfiguration> beanConfigurations = new ArrayList<BeanConfiguration>();
752 
753         public void add(Statement statement) {
754 
755             statements.add(statement); //we execute bean configuration statements in the order they are declared.
756 
757             if (statement instanceof InstantiationStatement) {
758                 InstantiationStatement is = (InstantiationStatement)statement;
759                 beanConfigurations.add(new BeanConfiguration(is));
760             } else {
761                 AssignmentStatement as = (AssignmentStatement)statement;
762                 //statements always apply to the most recently defined bean configuration with the same name, so we
763                 //have to traverse the configuration list starting at the end (most recent elements are appended):
764                 boolean addedToConfig = false;
765                 String beanName = as.getRootBeanName();
766                 for( int i = beanConfigurations.size()-1; i >= 0; i--) {
767                     BeanConfiguration mostRecent = beanConfigurations.get(i);
768                     String mostRecentBeanName = mostRecent.getBeanName();
769                     if (beanName.equals(mostRecentBeanName)) {
770                         mostRecent.add(as);
771                         addedToConfig = true;
772                         break;
773                     }
774                 }
775 
776                 if (!addedToConfig) {
777                     // the AssignmentStatement must be for an existing bean that does not yet have a corresponding
778                     // configuration object (this would happen if the bean is in the default objects map). Because
779                     // BeanConfiguration instances don't exist for default (already instantiated) beans,
780                     // we simulate a creation of one to satisfy this processors implementation:
781                     beanConfigurations.add(new BeanConfiguration(as));
782                 }
783             }
784         }
785 
786         public void execute() {
787 
788             for( Statement statement : statements) {
789 
790                 statement.execute();
791 
792                 BeanConfiguration bd = statement.getBeanConfiguration();
793 
794                 if (bd.isExecuted()) { //bean is fully configured, no more statements to execute for it:
795 
796                     //bean configured overrides the 'eventBus' bean - replace the existing eventBus with the one configured:
797                     if (bd.getBeanName().equals(EVENT_BUS_NAME)) {
798                         EventBus eventBus = (EventBus)bd.getBean();
799                         enableEvents(eventBus);
800                     }
801 
802                     //ignore global 'shiro.' shortcut mechanism:
803                     if (!bd.isGlobalConfig()) {
804                         BeanEvent event = new ConfiguredBeanEvent(bd.getBeanName(), bd.getBean(),
805                                 Collections.unmodifiableMap(objects));
806                         eventBus.publish(event);
807                     }
808 
809                     //initialize the bean if necessary:
810                     LifecycleUtils.init(bd.getBean());
811 
812                     //ignore global 'shiro.' shortcut mechanism:
813                     if (!bd.isGlobalConfig()) {
814                         BeanEvent event = new InitializedBeanEvent(bd.getBeanName(), bd.getBean(),
815                                 Collections.unmodifiableMap(objects));
816                         eventBus.publish(event);
817                     }
818                 }
819             }
820         }
821     }
822 
823     private class BeanConfiguration {
824 
825         private final InstantiationStatement instantiationStatement;
826         private final List<AssignmentStatement> assignments = new ArrayList<AssignmentStatement>();
827         private final String beanName;
828         private Object bean;
829 
830         private BeanConfiguration(InstantiationStatement statement) {
831             statement.setBeanConfiguration(this);
832             this.instantiationStatement = statement;
833             this.beanName = statement.lhs;
834         }
835 
836         private BeanConfiguration(AssignmentStatement as) {
837             this.instantiationStatement = null;
838             this.beanName = as.getRootBeanName();
839             add(as);
840         }
841 
842         public String getBeanName() {
843             return this.beanName;
844         }
845 
846         public boolean isGlobalConfig() { //BeanConfiguration instance representing the global 'shiro.' properties
847             // (we should remove this concept).
848             return GLOBAL_PROPERTY_PREFIX.equals(getBeanName());
849         }
850 
851         public void add(AssignmentStatement as) {
852             as.setBeanConfiguration(this);
853             assignments.add(as);
854         }
855 
856         /**
857          * When this configuration is parsed sufficiently to create (or find) an actual bean instance, that instance
858          * will be associated with its configuration by setting it via this method.
859          *
860          * @param bean the bean instantiated (or found) that corresponds to this BeanConfiguration instance.
861          */
862         public void setBean(Object bean) {
863             this.bean = bean;
864         }
865 
866         public Object getBean() {
867             return this.bean;
868         }
869 
870         /**
871          * Returns true if all configuration statements have been executed.
872          * @return true if all configuration statements have been executed.
873          */
874         public boolean isExecuted() {
875             if (instantiationStatement != null && !instantiationStatement.isExecuted()) {
876                 return false;
877             }
878             for (AssignmentStatement as : assignments) {
879                 if (!as.isExecuted()) {
880                     return false;
881                 }
882             }
883             return true;
884         }
885     }
886 
887     private abstract class Statement {
888 
889         protected final String lhs;
890         protected final String rhs;
891         protected Object bean;
892         private Object result;
893         private boolean executed;
894         private BeanConfiguration beanConfiguration;
895 
896         private Statement(String lhs, String rhs) {
897             this.lhs = lhs;
898             this.rhs = rhs;
899             this.executed = false;
900         }
901 
902         public void setBeanConfiguration(BeanConfiguration bd) {
903             this.beanConfiguration = bd;
904         }
905 
906         public BeanConfiguration getBeanConfiguration() {
907             return this.beanConfiguration;
908         }
909 
910         public Object execute() {
911             if (!isExecuted()) {
912                 this.result = doExecute();
913                 this.executed = true;
914             }
915             if (!getBeanConfiguration().isGlobalConfig()) {
916                 Assert.notNull(this.bean, "Implementation must set the root bean for which it executed.");
917             }
918             return this.result;
919         }
920 
921         public Object getBean() {
922             return this.bean;
923         }
924 
925         protected void setBean(Object bean) {
926             this.bean = bean;
927             if (this.beanConfiguration.getBean() == null) {
928                 this.beanConfiguration.setBean(bean);
929             }
930         }
931 
932         public Object getResult() {
933             return result;
934         }
935 
936         protected abstract Object doExecute();
937 
938         public boolean isExecuted() {
939             return executed;
940         }
941     }
942 
943     private class InstantiationStatement extends Statement {
944 
945         private InstantiationStatement(String lhs, String rhs) {
946             super(lhs, rhs);
947         }
948 
949         @Override
950         protected Object doExecute() {
951             String beanName = this.lhs;
952             createNewInstance(objects, beanName, this.rhs);
953             Object instantiated = objects.get(beanName);
954             setBean(instantiated);
955 
956             //also ensure the instantiated bean has access to the event bus or is subscribed to events if necessary:
957             //Note: because events are being enabled on this bean here (before the instantiated event below is
958             //triggered), beans can react to their own instantiation events.
959             enableEventsIfNecessary(instantiated, beanName);
960 
961             BeanEvent event = new InstantiatedBeanEvent(beanName, instantiated, Collections.unmodifiableMap(objects));
962             eventBus.publish(event);
963 
964             return instantiated;
965         }
966     }
967 
968     private class AssignmentStatement extends Statement {
969 
970         private final String rootBeanName;
971 
972         private AssignmentStatement(String lhs, String rhs) {
973             super(lhs, rhs);
974             int index = lhs.indexOf('.');
975             this.rootBeanName = lhs.substring(0, index);
976         }
977 
978         @Override
979         protected Object doExecute() {
980             applyProperty(lhs, rhs, objects);
981             Object bean = objects.get(this.rootBeanName);
982             setBean(bean);
983             return null;
984         }
985 
986         public String getRootBeanName() {
987             return this.rootBeanName;
988         }
989     }
990 
991     //////////////////////////
992     // From CollectionUtils //
993     //////////////////////////
994     // CollectionUtils cannot be removed from shiro-core until 2.0 as it has a dependency on PrincipalCollection
995 
996     private static boolean isEmpty(Map m) {
997         return m == null || m.isEmpty();
998     }
999 
1000     private static boolean isEmpty(Collection c) {
1001         return c == null || c.isEmpty();
1002     }
1003 
1004 }