001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.config;
020
021import org.apache.commons.beanutils.BeanUtils;
022import org.apache.commons.beanutils.PropertyUtils;
023import org.apache.shiro.codec.Base64;
024import org.apache.shiro.codec.Hex;
025import org.apache.shiro.config.event.BeanEvent;
026import org.apache.shiro.config.event.ConfiguredBeanEvent;
027import org.apache.shiro.config.event.DestroyedBeanEvent;
028import org.apache.shiro.config.event.InitializedBeanEvent;
029import org.apache.shiro.config.event.InstantiatedBeanEvent;
030import org.apache.shiro.event.EventBus;
031import org.apache.shiro.event.EventBusAware;
032import org.apache.shiro.event.Subscribe;
033import org.apache.shiro.event.support.DefaultEventBus;
034import org.apache.shiro.util.Assert;
035import org.apache.shiro.util.ByteSource;
036import org.apache.shiro.util.ClassUtils;
037import org.apache.shiro.util.CollectionUtils;
038import org.apache.shiro.util.Factory;
039import org.apache.shiro.util.LifecycleUtils;
040import org.apache.shiro.util.Nameable;
041import org.apache.shiro.util.StringUtils;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import java.beans.PropertyDescriptor;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Collection;
049import java.util.Collections;
050import java.util.LinkedHashMap;
051import java.util.LinkedHashSet;
052import java.util.List;
053import java.util.Map;
054import java.util.Set;
055
056
057/**
058 * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
059 * map of "property values".  Typically these come from the Shiro INI configuration and are used
060 * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
061 * <p/>
062 * Recognizes {@link Factory} implementations and will call
063 * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
064 *
065 * @since 0.9
066 */
067public class ReflectionBuilder {
068
069    //TODO - complete JavaDoc
070
071    private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
072
073    private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
074    private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
075    private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
076    private static final char MAP_KEY_VALUE_DELIMITER = ':';
077    private static final String HEX_BEGIN_TOKEN = "0x";
078    private static final String NULL_VALUE_TOKEN = "null";
079    private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
080    private static final char STRING_VALUE_DELIMETER = '"';
081    private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
082    private static final char MAP_PROPERTY_END_TOKEN = ']';
083
084    private static final String EVENT_BUS_NAME = "eventBus";
085
086    private final Map<String, Object> objects;
087    /**
088     * @since 1.3
089     */
090    private EventBus eventBus;
091    /**
092     * Keeps track of event subscribers that were automatically registered by this ReflectionBuilder during
093     * object construction.  This is used in case a new EventBus is discovered during object graph
094     * construction:  upon discovery of the new EventBus, the existing subscribers will be unregistered from the
095     * old EventBus and then re-registered with the new EventBus.
096     *
097     * @since 1.3
098     */
099    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}