/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.config; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.SuppressPropertiesBeanIntrospector; import org.apache.shiro.codec.Base64; import org.apache.shiro.codec.Hex; import org.apache.shiro.config.event.BeanEvent; import org.apache.shiro.config.event.ConfiguredBeanEvent; import org.apache.shiro.config.event.DestroyedBeanEvent; import org.apache.shiro.config.event.InitializedBeanEvent; import org.apache.shiro.config.event.InstantiatedBeanEvent; import org.apache.shiro.event.EventBus; import org.apache.shiro.event.EventBusAware; import org.apache.shiro.event.Subscribe; import org.apache.shiro.event.support.DefaultEventBus; import org.apache.shiro.util.Assert; import org.apache.shiro.util.ByteSource; import org.apache.shiro.util.ClassUtils; import org.apache.shiro.util.Factory; import org.apache.shiro.util.LifecycleUtils; import org.apache.shiro.util.Nameable; import org.apache.shiro.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.beans.PropertyDescriptor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Object builder that uses reflection and Apache Commons BeanUtils to build objects given a * map of "property values". Typically these come from the Shiro INI configuration and are used * to construct or modify the SecurityManager, its dependencies, and web-based security filters. * <p/> * Recognizes {@link Factory} implementations and will call * {@link org.apache.shiro.util.Factory#getInstance() getInstance} to satisfy any reference to this bean. * * @since 0.9 */ public class ReflectionBuilder { //TODO - complete JavaDoc private static final Logger log = LoggerFactory.getLogger(ReflectionBuilder.class); private static final String OBJECT_REFERENCE_BEGIN_TOKEN = "$"; private static final String ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN = "\\$"; private static final String GLOBAL_PROPERTY_PREFIX = "shiro"; private static final char MAP_KEY_VALUE_DELIMITER = ':'; private static final String HEX_BEGIN_TOKEN = "0x"; private static final String NULL_VALUE_TOKEN = "null"; private static final String EMPTY_STRING_VALUE_TOKEN = "\"\""; private static final char STRING_VALUE_DELIMETER = '"'; private static final char MAP_PROPERTY_BEGIN_TOKEN = '['; private static final char MAP_PROPERTY_END_TOKEN = ']'; private static final String EVENT_BUS_NAME = "eventBus"; private final Map<String, Object> objects; /** * Interpolation allows for ${key} substitution of values. * @since 1.4 */ private Interpolator interpolator; /** * @since 1.3 */ private EventBus eventBus; /** * Keeps track of event subscribers that were automatically registered by this ReflectionBuilder during * object construction. This is used in case a new EventBus is discovered during object graph * construction: upon discovery of the new EventBus, the existing subscribers will be unregistered from the * old EventBus and then re-registered with the new EventBus. * * @since 1.3 */ private final Map<String,Object> registeredEventSubscribers; /** * @since 1.4 */ private final BeanUtilsBean beanUtilsBean; //@since 1.3 private Map<String,Object> createDefaultObjectMap() { Map<String,Object> map = new LinkedHashMap<String, Object>(); map.put(EVENT_BUS_NAME, new DefaultEventBus()); return map; } public ReflectionBuilder() { this(null); } public ReflectionBuilder(Map<String, ?> defaults) { // SHIRO-619 beanUtilsBean = new BeanUtilsBean(); beanUtilsBean.getPropertyUtils().addBeanIntrospector(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS); this.interpolator = createInterpolator(); this.objects = createDefaultObjectMap(); this.registeredEventSubscribers = new LinkedHashMap<String,Object>(); apply(defaults); } private void apply(Map<String, ?> objects) { if(!isEmpty(objects)) { this.objects.putAll(objects); } EventBus found = findEventBus(this.objects); Assert.notNull(found, "An " + EventBus.class.getName() + " instance must be present in the object defaults"); enableEvents(found); } public Map<String, ?> getObjects() { return objects; } /** * @param objects */ public void setObjects(Map<String, ?> objects) { this.objects.clear(); this.objects.putAll(createDefaultObjectMap()); apply(objects); } //@since 1.3 private void enableEvents(EventBus eventBus) { Assert.notNull(eventBus, "EventBus argument cannot be null."); //clean up old auto-registered subscribers: for (Object subscriber : this.registeredEventSubscribers.values()) { this.eventBus.unregister(subscriber); } this.registeredEventSubscribers.clear(); this.eventBus = eventBus; for(Map.Entry<String,Object> entry : this.objects.entrySet()) { enableEventsIfNecessary(entry.getValue(), entry.getKey()); } } //@since 1.3 private void enableEventsIfNecessary(Object bean, String name) { boolean applied = applyEventBusIfNecessary(bean); if (!applied) { //if the event bus is applied, and the bean wishes to be a subscriber as well (not just a publisher), // we assume that the implementation registers itself with the event bus, i.e. eventBus.register(this); //if the event bus isn't applied, only then do we need to check to see if the bean is an event subscriber, // and if so, register it on the event bus automatically since it has no ability to do so itself: if (isEventSubscriber(bean, name)) { //found an event subscriber, so register them with the EventBus: this.eventBus.register(bean); this.registeredEventSubscribers.put(name, bean); } } } //@since 1.3 private boolean isEventSubscriber(Object bean, String name) { List annotatedMethods = ClassUtils.getAnnotatedMethods(bean.getClass(), Subscribe.class); return !isEmpty(annotatedMethods); } //@since 1.3 protected EventBus findEventBus(Map<String,?> objects) { if (isEmpty(objects)) { return null; } //prefer a named object first: Object value = objects.get(EVENT_BUS_NAME); if (value != null && value instanceof EventBus) { return (EventBus)value; } //couldn't find a named 'eventBus' EventBus object. Try to find the first typed value we can: for( Object v : objects.values()) { if (v instanceof EventBus) { return (EventBus)v; } } return null; } private boolean applyEventBusIfNecessary(Object value) { if (value instanceof EventBusAware) { ((EventBusAware)value).setEventBus(this.eventBus); return true; } return false; } public Object getBean(String id) { return objects.get(id); } @SuppressWarnings({"unchecked"}) public <T> T getBean(String id, Class<T> requiredType) { if (requiredType == null) { throw new NullPointerException("requiredType argument cannot be null."); } Object bean = getBean(id); if (bean == null) { return null; } Assert.state(requiredType.isAssignableFrom(bean.getClass()), "Bean with id [" + id + "] is not of the required type [" + requiredType.getName() + "]."); return (T) bean; } private String parseBeanId(String lhs) { Assert.notNull(lhs); if (lhs.indexOf('.') < 0) { return lhs; } String classSuffix = ".class"; int index = lhs.indexOf(classSuffix); if (index >= 0) { return lhs.substring(0, index); } return null; } @SuppressWarnings({"unchecked"}) public Map<String, ?> buildObjects(Map<String, String> kvPairs) { if (kvPairs != null && !kvPairs.isEmpty()) { BeanConfigurationProcessor processor = new BeanConfigurationProcessor(); for (Map.Entry<String, String> entry : kvPairs.entrySet()) { String lhs = entry.getKey(); String rhs = interpolator.interpolate(entry.getValue()); String beanId = parseBeanId(lhs); if (beanId != null) { //a beanId could be parsed, so the line is a bean instance definition processor.add(new InstantiationStatement(beanId, rhs)); } else { //the line must be a property configuration processor.add(new AssignmentStatement(lhs, rhs)); } } processor.execute(); } //SHIRO-413: init method must be called for constructed objects that are Initializable LifecycleUtils.init(objects.values()); return objects; } public void destroy() { final Map<String, Object> immutableObjects = Collections.unmodifiableMap(objects); //destroy objects in the opposite order they were initialized: List<Map.Entry<String,?>> entries = new ArrayList<Map.Entry<String,?>>(objects.entrySet()); Collections.reverse(entries); for(Map.Entry<String, ?> entry: entries) { String id = entry.getKey(); Object bean = entry.getValue(); //don't destroy the eventbus until the end - we need it to still be 'alive' while publishing destroy events: if (bean != this.eventBus) { //memory equality check (not .equals) on purpose LifecycleUtils.destroy(bean); BeanEvent event = new DestroyedBeanEvent(id, bean, immutableObjects); eventBus.publish(event); this.eventBus.unregister(bean); //bean is now destroyed - it should not receive any other events } } //only now destroy the event bus: LifecycleUtils.destroy(this.eventBus); } protected void createNewInstance(Map<String, Object> objects, String name, String value) { Object currentInstance = objects.get(name); if (currentInstance != null) { log.info("An instance with name '{}' already exists. " + "Redefining this object as a new instance of type {}", name, value); } Object instance;//name with no property, assume right hand side of equals sign is the class name: try { instance = ClassUtils.newInstance(value); if (instance instanceof Nameable) { ((Nameable) instance).setName(name); } } catch (Exception e) { String msg = "Unable to instantiate class [" + value + "] for object named '" + name + "'. " + "Please ensure you've specified the fully qualified class name correctly."; throw new ConfigurationException(msg, e); } objects.put(name, instance); } protected void applyProperty(String key, String value, Map objects) { int index = key.indexOf('.'); if (index >= 0) { String name = key.substring(0, index); String property = key.substring(index + 1, key.length()); if (GLOBAL_PROPERTY_PREFIX.equalsIgnoreCase(name)) { applyGlobalProperty(objects, property, value); } else { applySingleProperty(objects, name, property, value); } } else { throw new IllegalArgumentException("All property keys must contain a '.' character. " + "(e.g. myBean.property = value) These should already be separated out by buildObjects()."); } } protected void applyGlobalProperty(Map objects, String property, String value) { for (Object instance : objects.values()) { try { PropertyDescriptor pd = beanUtilsBean.getPropertyUtils().getPropertyDescriptor(instance, property); if (pd != null) { applyProperty(instance, property, value); } } catch (Exception e) { String msg = "Error retrieving property descriptor for instance " + "of type [" + instance.getClass().getName() + "] " + "while setting property [" + property + "]"; throw new ConfigurationException(msg, e); } } } protected void applySingleProperty(Map objects, String name, String property, String value) { Object instance = objects.get(name); if (property.equals("class")) { throw new IllegalArgumentException("Property keys should not contain 'class' properties since these " + "should already be separated out by buildObjects()."); } else if (instance == null) { String msg = "Configuration error. Specified object [" + name + "] with property [" + property + "] without first defining that object's class. Please first " + "specify the class property first, e.g. myObject = fully_qualified_class_name " + "and then define additional properties."; throw new IllegalArgumentException(msg); } else { applyProperty(instance, property, value); } } protected boolean isReference(String value) { return value != null && value.startsWith(OBJECT_REFERENCE_BEGIN_TOKEN); } protected String getId(String referenceToken) { return referenceToken.substring(OBJECT_REFERENCE_BEGIN_TOKEN.length()); } protected Object getReferencedObject(String id) { Object o = objects != null && !objects.isEmpty() ? objects.get(id) : null; if (o == null) { String msg = "The object with id [" + id + "] has not yet been defined and therefore cannot be " + "referenced. Please ensure objects are defined in the order in which they should be " + "created and made available for future reference."; throw new UnresolveableReferenceException(msg); } return o; } protected String unescapeIfNecessary(String value) { if (value != null && value.startsWith(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN)) { return value.substring(ESCAPED_OBJECT_REFERENCE_BEGIN_TOKEN.length() - 1); } return value; } protected Object resolveReference(String reference) { String id = getId(reference); log.debug("Encountered object reference '{}'. Looking up object with id '{}'", reference, id); final Object referencedObject = getReferencedObject(id); if (referencedObject instanceof Factory) { return ((Factory) referencedObject).getInstance(); } return referencedObject; } protected boolean isTypedProperty(Object object, String propertyName, Class clazz) { if (clazz == null) { throw new NullPointerException("type (class) argument cannot be null."); } try { PropertyDescriptor descriptor = beanUtilsBean.getPropertyUtils().getPropertyDescriptor(object, propertyName); if (descriptor == null) { String msg = "Property '" + propertyName + "' does not exist for object of " + "type " + object.getClass().getName() + "."; throw new ConfigurationException(msg); } Class propertyClazz = descriptor.getPropertyType(); return clazz.isAssignableFrom(propertyClazz); } catch (ConfigurationException ce) { //let it propagate: throw ce; } catch (Exception e) { String msg = "Unable to determine if property [" + propertyName + "] represents a " + clazz.getName(); throw new ConfigurationException(msg, e); } } protected Set<?> toSet(String sValue) { String[] tokens = StringUtils.split(sValue); if (tokens == null || tokens.length <= 0) { return null; } //SHIRO-423: check to see if the value is a referenced Set already, and if so, return it immediately: if (tokens.length == 1 && isReference(tokens[0])) { Object reference = resolveReference(tokens[0]); if (reference instanceof Set) { return (Set)reference; } } Set<String> setTokens = new LinkedHashSet<String>(Arrays.asList(tokens)); //now convert into correct values and/or references: Set<Object> values = new LinkedHashSet<Object>(setTokens.size()); for (String token : setTokens) { Object value = resolveValue(token); values.add(value); } return values; } protected Map<?, ?> toMap(String sValue) { String[] tokens = StringUtils.split(sValue, StringUtils.DEFAULT_DELIMITER_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, StringUtils.DEFAULT_QUOTE_CHAR, true, true); if (tokens == null || tokens.length <= 0) { return null; } //SHIRO-423: check to see if the value is a referenced Map already, and if so, return it immediately: if (tokens.length == 1 && isReference(tokens[0])) { Object reference = resolveReference(tokens[0]); if (reference instanceof Map) { return (Map)reference; } } Map<String, String> mapTokens = new LinkedHashMap<String, String>(tokens.length); for (String token : tokens) { String[] kvPair = StringUtils.split(token, MAP_KEY_VALUE_DELIMITER); if (kvPair == null || kvPair.length != 2) { String msg = "Map property value [" + sValue + "] contained key-value pair token [" + token + "] that does not properly split to a single key and pair. This must be the " + "case for all map entries."; throw new ConfigurationException(msg); } mapTokens.put(kvPair[0], kvPair[1]); } //now convert into correct values and/or references: Map<Object, Object> map = new LinkedHashMap<Object, Object>(mapTokens.size()); for (Map.Entry<String, String> entry : mapTokens.entrySet()) { Object key = resolveValue(entry.getKey()); Object value = resolveValue(entry.getValue()); map.put(key, value); } return map; } // @since 1.2.2 protected Collection<?> toCollection(String sValue) { String[] tokens = StringUtils.split(sValue); if (tokens == null || tokens.length <= 0) { return null; } //SHIRO-423: check to see if the value is a referenced Collection already, and if so, return it immediately: if (tokens.length == 1 && isReference(tokens[0])) { Object reference = resolveReference(tokens[0]); if (reference instanceof Collection) { return (Collection)reference; } } //now convert into correct values and/or references: List<Object> values = new ArrayList<Object>(tokens.length); for (String token : tokens) { Object value = resolveValue(token); values.add(value); } return values; } protected List<?> toList(String sValue) { String[] tokens = StringUtils.split(sValue); if (tokens == null || tokens.length <= 0) { return null; } //SHIRO-423: check to see if the value is a referenced List already, and if so, return it immediately: if (tokens.length == 1 && isReference(tokens[0])) { Object reference = resolveReference(tokens[0]); if (reference instanceof List) { return (List)reference; } } //now convert into correct values and/or references: List<Object> values = new ArrayList<Object>(tokens.length); for (String token : tokens) { Object value = resolveValue(token); values.add(value); } return values; } protected byte[] toBytes(String sValue) { if (sValue == null) { return null; } byte[] bytes; if (sValue.startsWith(HEX_BEGIN_TOKEN)) { String hex = sValue.substring(HEX_BEGIN_TOKEN.length()); bytes = Hex.decode(hex); } else { //assume base64 encoded: bytes = Base64.decode(sValue); } return bytes; } protected Object resolveValue(String stringValue) { Object value; if (isReference(stringValue)) { value = resolveReference(stringValue); } else { value = unescapeIfNecessary(stringValue); } return value; } protected String checkForNullOrEmptyLiteral(String stringValue) { if (stringValue == null) { return null; } //check if the value is the actual literal string 'null' (expected to be wrapped in quotes): if (stringValue.equals("\"null\"")) { return NULL_VALUE_TOKEN; } //or the actual literal string of two quotes '""' (expected to be wrapped in quotes): else if (stringValue.equals("\"\"\"\"")) { return EMPTY_STRING_VALUE_TOKEN; } else { return stringValue; } } protected void applyProperty(Object object, String propertyPath, Object value) { int mapBegin = propertyPath.indexOf(MAP_PROPERTY_BEGIN_TOKEN); int mapEnd = -1; String mapPropertyPath = null; String keyString = null; String remaining = null; if (mapBegin >= 0) { //a map is being referenced in the overall property path. Find just the map's path: mapPropertyPath = propertyPath.substring(0, mapBegin); //find the end of the map reference: mapEnd = propertyPath.indexOf(MAP_PROPERTY_END_TOKEN, mapBegin); //find the token in between the [ and the ] (the map/array key or index): keyString = propertyPath.substring(mapBegin+1, mapEnd); //find out if there is more path reference to follow. If not, we're at a terminal of the OGNL expression if (propertyPath.length() > (mapEnd+1)) { remaining = propertyPath.substring(mapEnd+1); if (remaining.startsWith(".")) { remaining = StringUtils.clean(remaining.substring(1)); } } } if (remaining == null) { //we've terminated the OGNL expression. Check to see if we're assigning a property or a map entry: if (keyString == null) { //not a map or array value assignment - assign the property directly: setProperty(object, propertyPath, value); } else { //we're assigning a map or array entry. Check to see which we should call: if (isTypedProperty(object, mapPropertyPath, Map.class)) { Map map = (Map)getProperty(object, mapPropertyPath); Object mapKey = resolveValue(keyString); //noinspection unchecked map.put(mapKey, value); } else { //must be an array property. Convert the key string to an index: int index = Integer.valueOf(keyString); setIndexedProperty(object, mapPropertyPath, index, value); } } } else { //property is being referenced as part of a nested path. Find the referenced map/array entry and //recursively call this method with the remaining property path Object referencedValue = null; if (isTypedProperty(object, mapPropertyPath, Map.class)) { Map map = (Map)getProperty(object, mapPropertyPath); Object mapKey = resolveValue(keyString); referencedValue = map.get(mapKey); } else { //must be an array property: int index = Integer.valueOf(keyString); referencedValue = getIndexedProperty(object, mapPropertyPath, index); } if (referencedValue == null) { throw new ConfigurationException("Referenced map/array value '" + mapPropertyPath + "[" + keyString + "]' does not exist."); } applyProperty(referencedValue, remaining, value); } } private void setProperty(Object object, String propertyPath, Object value) { try { if (log.isTraceEnabled()) { log.trace("Applying property [{}] value [{}] on object of type [{}]", new Object[]{propertyPath, value, object.getClass().getName()}); } beanUtilsBean.setProperty(object, propertyPath, value); } catch (Exception e) { String msg = "Unable to set property '" + propertyPath + "' with value [" + value + "] on object " + "of type " + (object != null ? object.getClass().getName() : null) + ". If " + "'" + value + "' is a reference to another (previously defined) object, prefix it with " + "'" + OBJECT_REFERENCE_BEGIN_TOKEN + "' to indicate that the referenced " + "object should be used as the actual value. " + "For example, " + OBJECT_REFERENCE_BEGIN_TOKEN + value; throw new ConfigurationException(msg, e); } } private Object getProperty(Object object, String propertyPath) { try { return beanUtilsBean.getPropertyUtils().getProperty(object, propertyPath); } catch (Exception e) { throw new ConfigurationException("Unable to access property '" + propertyPath + "'", e); } } private void setIndexedProperty(Object object, String propertyPath, int index, Object value) { try { beanUtilsBean.getPropertyUtils().setIndexedProperty(object, propertyPath, index, value); } catch (Exception e) { throw new ConfigurationException("Unable to set array property '" + propertyPath + "'", e); } } private Object getIndexedProperty(Object object, String propertyPath, int index) { try { return beanUtilsBean.getPropertyUtils().getIndexedProperty(object, propertyPath, index); } catch (Exception e) { throw new ConfigurationException("Unable to acquire array property '" + propertyPath + "'", e); } } protected boolean isIndexedPropertyAssignment(String propertyPath) { return propertyPath.endsWith("" + MAP_PROPERTY_END_TOKEN); } protected void applyProperty(Object object, String propertyName, String stringValue) { Object value; if (NULL_VALUE_TOKEN.equals(stringValue)) { value = null; } else if (EMPTY_STRING_VALUE_TOKEN.equals(stringValue)) { value = StringUtils.EMPTY_STRING; } else if (isIndexedPropertyAssignment(propertyName)) { String checked = checkForNullOrEmptyLiteral(stringValue); value = resolveValue(checked); } else if (isTypedProperty(object, propertyName, Set.class)) { value = toSet(stringValue); } else if (isTypedProperty(object, propertyName, Map.class)) { value = toMap(stringValue); } else if (isTypedProperty(object, propertyName, List.class)) { value = toList(stringValue); } else if (isTypedProperty(object, propertyName, Collection.class)) { value = toCollection(stringValue); } else if (isTypedProperty(object, propertyName, byte[].class)) { value = toBytes(stringValue); } else if (isTypedProperty(object, propertyName, ByteSource.class)) { byte[] bytes = toBytes(stringValue); value = ByteSource.Util.bytes(bytes); } else { String checked = checkForNullOrEmptyLiteral(stringValue); value = resolveValue(checked); } applyProperty(object, propertyName, value); } private Interpolator createInterpolator() { if (ClassUtils.isAvailable("org.apache.commons.configuration2.interpol.ConfigurationInterpolator")) { return new CommonsInterpolator(); } return new DefaultInterpolator(); } /** * Sets the {@link Interpolator} used when evaluating the right side of the expressions. * @since 1.4 */ public void setInterpolator(Interpolator interpolator) { this.interpolator = interpolator; } private class BeanConfigurationProcessor { private final List<Statement> statements = new ArrayList<Statement>(); private final List<BeanConfiguration> beanConfigurations = new ArrayList<BeanConfiguration>(); public void add(Statement statement) { statements.add(statement); //we execute bean configuration statements in the order they are declared. if (statement instanceof InstantiationStatement) { InstantiationStatement is = (InstantiationStatement)statement; beanConfigurations.add(new BeanConfiguration(is)); } else { AssignmentStatement as = (AssignmentStatement)statement; //statements always apply to the most recently defined bean configuration with the same name, so we //have to traverse the configuration list starting at the end (most recent elements are appended): boolean addedToConfig = false; String beanName = as.getRootBeanName(); for( int i = beanConfigurations.size()-1; i >= 0; i--) { BeanConfiguration mostRecent = beanConfigurations.get(i); String mostRecentBeanName = mostRecent.getBeanName(); if (beanName.equals(mostRecentBeanName)) { mostRecent.add(as); addedToConfig = true; break; } } if (!addedToConfig) { // the AssignmentStatement must be for an existing bean that does not yet have a corresponding // configuration object (this would happen if the bean is in the default objects map). Because // BeanConfiguration instances don't exist for default (already instantiated) beans, // we simulate a creation of one to satisfy this processors implementation: beanConfigurations.add(new BeanConfiguration(as)); } } } public void execute() { for( Statement statement : statements) { statement.execute(); BeanConfiguration bd = statement.getBeanConfiguration(); if (bd.isExecuted()) { //bean is fully configured, no more statements to execute for it: //bean configured overrides the 'eventBus' bean - replace the existing eventBus with the one configured: if (bd.getBeanName().equals(EVENT_BUS_NAME)) { EventBus eventBus = (EventBus)bd.getBean(); enableEvents(eventBus); } //ignore global 'shiro.' shortcut mechanism: if (!bd.isGlobalConfig()) { BeanEvent event = new ConfiguredBeanEvent(bd.getBeanName(), bd.getBean(), Collections.unmodifiableMap(objects)); eventBus.publish(event); } //initialize the bean if necessary: LifecycleUtils.init(bd.getBean()); //ignore global 'shiro.' shortcut mechanism: if (!bd.isGlobalConfig()) { BeanEvent event = new InitializedBeanEvent(bd.getBeanName(), bd.getBean(), Collections.unmodifiableMap(objects)); eventBus.publish(event); } } } } } private class BeanConfiguration { private final InstantiationStatement instantiationStatement; private final List<AssignmentStatement> assignments = new ArrayList<AssignmentStatement>(); private final String beanName; private Object bean; private BeanConfiguration(InstantiationStatement statement) { statement.setBeanConfiguration(this); this.instantiationStatement = statement; this.beanName = statement.lhs; } private BeanConfiguration(AssignmentStatement as) { this.instantiationStatement = null; this.beanName = as.getRootBeanName(); add(as); } public String getBeanName() { return this.beanName; } public boolean isGlobalConfig() { //BeanConfiguration instance representing the global 'shiro.' properties // (we should remove this concept). return GLOBAL_PROPERTY_PREFIX.equals(getBeanName()); } public void add(AssignmentStatement as) { as.setBeanConfiguration(this); assignments.add(as); } /** * When this configuration is parsed sufficiently to create (or find) an actual bean instance, that instance * will be associated with its configuration by setting it via this method. * * @param bean the bean instantiated (or found) that corresponds to this BeanConfiguration instance. */ public void setBean(Object bean) { this.bean = bean; } public Object getBean() { return this.bean; } /** * Returns true if all configuration statements have been executed. * @return true if all configuration statements have been executed. */ public boolean isExecuted() { if (instantiationStatement != null && !instantiationStatement.isExecuted()) { return false; } for (AssignmentStatement as : assignments) { if (!as.isExecuted()) { return false; } } return true; } } private abstract class Statement { protected final String lhs; protected final String rhs; protected Object bean; private Object result; private boolean executed; private BeanConfiguration beanConfiguration; private Statement(String lhs, String rhs) { this.lhs = lhs; this.rhs = rhs; this.executed = false; } public void setBeanConfiguration(BeanConfiguration bd) { this.beanConfiguration = bd; } public BeanConfiguration getBeanConfiguration() { return this.beanConfiguration; } public Object execute() { if (!isExecuted()) { this.result = doExecute(); this.executed = true; } if (!getBeanConfiguration().isGlobalConfig()) { Assert.notNull(this.bean, "Implementation must set the root bean for which it executed."); } return this.result; } public Object getBean() { return this.bean; } protected void setBean(Object bean) { this.bean = bean; if (this.beanConfiguration.getBean() == null) { this.beanConfiguration.setBean(bean); } } public Object getResult() { return result; } protected abstract Object doExecute(); public boolean isExecuted() { return executed; } } private class InstantiationStatement extends Statement { private InstantiationStatement(String lhs, String rhs) { super(lhs, rhs); } @Override protected Object doExecute() { String beanName = this.lhs; createNewInstance(objects, beanName, this.rhs); Object instantiated = objects.get(beanName); setBean(instantiated); //also ensure the instantiated bean has access to the event bus or is subscribed to events if necessary: //Note: because events are being enabled on this bean here (before the instantiated event below is //triggered), beans can react to their own instantiation events. enableEventsIfNecessary(instantiated, beanName); BeanEvent event = new InstantiatedBeanEvent(beanName, instantiated, Collections.unmodifiableMap(objects)); eventBus.publish(event); return instantiated; } } private class AssignmentStatement extends Statement { private final String rootBeanName; private AssignmentStatement(String lhs, String rhs) { super(lhs, rhs); int index = lhs.indexOf('.'); this.rootBeanName = lhs.substring(0, index); } @Override protected Object doExecute() { applyProperty(lhs, rhs, objects); Object bean = objects.get(this.rootBeanName); setBean(bean); return null; } public String getRootBeanName() { return this.rootBeanName; } } ////////////////////////// // From CollectionUtils // ////////////////////////// // CollectionUtils cannot be removed from shiro-core until 2.0 as it has a dependency on PrincipalCollection private static boolean isEmpty(Map m) { return m == null || m.isEmpty(); } private static boolean isEmpty(Collection c) { return c == null || c.isEmpty(); } }