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 java.beans.PropertyDescriptor;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.LinkedHashMap;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import org.apache.commons.beanutils.BeanUtilsBean;
032import org.apache.commons.beanutils.ConvertUtilsBean;
033import org.apache.commons.beanutils.SuppressPropertiesBeanIntrospector;
034import org.apache.shiro.codec.Base64;
035import org.apache.shiro.codec.Hex;
036import org.apache.shiro.config.event.BeanEvent;
037import org.apache.shiro.config.event.ConfiguredBeanEvent;
038import org.apache.shiro.config.event.DestroyedBeanEvent;
039import org.apache.shiro.config.event.InitializedBeanEvent;
040import org.apache.shiro.config.event.InstantiatedBeanEvent;
041import org.apache.shiro.event.EventBus;
042import org.apache.shiro.event.EventBusAware;
043import org.apache.shiro.event.Subscribe;
044import org.apache.shiro.event.support.DefaultEventBus;
045import org.apache.shiro.util.Assert;
046import org.apache.shiro.util.ByteSource;
047import org.apache.shiro.util.ClassUtils;
048import org.apache.shiro.util.Factory;
049import org.apache.shiro.util.LifecycleUtils;
050import org.apache.shiro.util.Nameable;
051import org.apache.shiro.util.StringUtils;
052import org.slf4j.Logger;
053import org.slf4j.LoggerFactory;
054
055
056/**
057 * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
058 * map of "property values".  Typically these come from the Shiro INI configuration and are used
059 * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
060 * <p/>
061 * Recognizes {@link Factory} implementations and will call
062 * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
063 *
064 * @since 0.9
065 */
066public class ReflectionBuilder {
067
068    //TODO - complete JavaDoc
069
070    private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class);
071
072    private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
073    private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
074    private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
075    private static final char MAP_KEY_VALUE_DELIMITER = ':';
076    private static final String HEX_BEGIN_TOKEN = "0x";
077    private static final String NULL_VALUE_TOKEN = "null";
078    private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
079    private static final char STRING_VALUE_DELIMETER = '"';
080    private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
081    private static final char MAP_PROPERTY_END_TOKEN = ']';
082
083    private static final String EVENT_BUS_NAME = "eventBus";
084
085    private final Map<String, Object> objects;
086
087    /**
088     * Interpolation allows for ${key} substitution of values.
089     * @since 1.4
090     */
091    private Interpolator interpolator;
092
093    /**
094     * @since 1.3
095     */
096    private EventBus eventBus;
097    /**
098     * Keeps track of event subscribers that were automatically registered by this ReflectionBuilder during
099     * 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}