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