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