View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.config.ogdl;
20  
21  import java.beans.PropertyDescriptor;
22  import java.util.ArrayList;
23  import java.util.Arrays;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.LinkedHashMap;
27  import java.util.LinkedHashSet;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.function.Function;
32  
33  import org.apache.commons.beanutils.BeanUtilsBean;
34  import org.apache.commons.beanutils.ConvertUtilsBean;
35  import org.apache.commons.beanutils.FluentPropertyBeanIntrospector;
36  import org.apache.commons.beanutils.SuppressPropertiesBeanIntrospector;
37  import org.apache.shiro.lang.codec.Base64;
38  import org.apache.shiro.lang.codec.Hex;
39  import org.apache.shiro.config.ConfigurationException;
40  import org.apache.shiro.config.ogdl.event.BeanEvent;
41  import org.apache.shiro.config.ogdl.event.ConfiguredBeanEvent;
42  import org.apache.shiro.config.ogdl.event.DestroyedBeanEvent;
43  import org.apache.shiro.config.ogdl.event.InitializedBeanEvent;
44  import org.apache.shiro.config.ogdl.event.InstantiatedBeanEvent;
45  import org.apache.shiro.event.EventBus;
46  import org.apache.shiro.event.EventBusAware;
47  import org.apache.shiro.event.Subscribe;
48  import org.apache.shiro.event.support.DefaultEventBus;
49  import org.apache.shiro.lang.util.Assert;
50  import org.apache.shiro.lang.util.ByteSource;
51  import org.apache.shiro.lang.util.ClassUtils;
52  import org.apache.shiro.lang.util.Factory;
53  import org.apache.shiro.lang.util.LifecycleUtils;
54  import org.apache.shiro.lang.util.Nameable;
55  import org.apache.shiro.lang.util.StringUtils;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  
60  /**
61   * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a
62   * map of "property values".  Typically these come from the Shiro INI configuration and are used
63   * to construct or modify the SecurityManager, its dependencies, and web-based security filters.
64   * <p/>
65   * Recognizes {@link Factory} implementations and will call
66   * {@link org.apache.shiro.lang.util.Factory#getInstance() getInstance} to satisfy any reference to this bean.
67   *
68   * @since 0.9
69   */
70  @SuppressWarnings("checkstyle:MethodCount")
71  public class ReflectionBuilder {
72  
73      private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionBuilder.class);
74  
75      private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$";
76      private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$";
77      private static final String GLOBAL_PROPERTY_PREFIX = "shiro";
78      private static final char MAP_KEY_VALUE_DELIMITER = ':';
79      private static final String HEX_BEGIN_TOKEN = "0x";
80      private static final String NULL_VALUE_TOKEN = "null";
81      private static final String EMPTY_STRING_VALUE_TOKEN = "\"\"";
82      private static final char STRING_VALUE_DELIMETER = '"';
83      private static final char MAP_PROPERTY_BEGIN_TOKEN = '[';
84      private static final char MAP_PROPERTY_END_TOKEN = ']';
85  
86      private static final String EVENT_BUS_NAME = "eventBus";
87  
88      private final Map<String, Object> objects;
89  
90      /**
91       * Interpolation allows for ${key} substitution of values.
92       *
93       * @since 1.4
94       */
95      private Interpolator interpolator;
96  
97      /**
98       * @since 1.3
99       */
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 }