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