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