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}