001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017–2019 microBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.configuration; 018 019import java.beans.FeatureDescriptor; 020 021import java.lang.reflect.Type; 022 023import java.util.Collection; 024import java.util.Collections; 025import java.util.Comparator; 026import java.util.HashMap; 027import java.util.HashSet; 028import java.util.Iterator; 029import java.util.LinkedList; 030import java.util.Map; 031import java.util.Objects; 032import java.util.PriorityQueue; 033import java.util.Queue; 034import java.util.ServiceLoader; 035import java.util.Set; 036import java.util.TreeSet; 037 038import java.util.function.Function; 039 040import java.util.logging.Level; 041import java.util.logging.Logger; 042 043import java.util.stream.Collectors; 044 045import javax.el.ELContext; 046import javax.el.ELResolver; 047import javax.el.ExpressionFactory; 048import javax.el.PropertyNotFoundException; 049import javax.el.StandardELContext; 050import javax.el.ValueExpression; 051 052import org.microbean.configuration.api.AmbiguousConfigurationValuesException; 053import org.microbean.configuration.api.ConfigurationException; 054import org.microbean.configuration.api.ConfigurationValue; 055import org.microbean.configuration.api.ConversionException; 056import org.microbean.configuration.api.TypeLiteral; 057 058import org.microbean.configuration.spi.Arbiter; 059import org.microbean.configuration.spi.Configuration; 060import org.microbean.configuration.spi.Converter; 061 062/** 063 * An implementation of the {@link 064 * org.microbean.configuration.api.Configurations} class that serves 065 * as a single source for configuration values suitable for an 066 * application. 067 * 068 * @author <a href="https://about.me/lairdnelson" 069 * target="_parent">Laird Nelson</a> 070 * 071 * @see Configuration 072 */ 073public class Configurations extends org.microbean.configuration.api.Configurations { 074 075 076 /* 077 * Static fields. 078 */ 079 080 081 /** 082 * A {@link Comparator} of {@link ConfigurationValue}s that reverse 083 * sorts them according to their {@linkplain 084 * ConfigurationValue#specificity() specificity}. 085 * 086 * <p>This field is never {@code null}.</p> 087 */ 088 private static final Comparator<ConfigurationValue> configurationValueComparator = Comparator.<ConfigurationValue>comparingInt(v -> v.specificity()).reversed(); 089 090 /** 091 * A {@link ThreadLocal} tracking a {@link Set} of {@link 092 * Configuration}s that are currently in the process of executing 093 * their {@link Configuration#getValue(Map, String)} methods. 094 * 095 * <p>This field is never {@code null} but its contents might be 096 * {@code null}.</p> 097 * 098 * @see #getValue(Map, String, Converter, String) 099 * 100 * @see #activate(Configuration) 101 * 102 * @see #deactivate(Configuration) 103 * 104 * @see #isActive(Configuration) 105 */ 106 private static final ThreadLocal<Map<Configurations, Set<Configuration>>> staticCurrentlyActiveConfigurations = ThreadLocal.withInitial(() -> new HashMap<>()); 107 108 /** 109 * A {@link ServiceLoader} instance used by the {@link 110 * #loadConfigurations()} method. 111 * 112 * <p>This field may be {@code null}.</p> 113 * 114 * @see #loadConfigurations() 115 * 116 * @see ServiceLoader 117 */ 118 private static volatile ServiceLoader<Configuration> configurationLoader; 119 120 /** 121 * A {@link ServiceLoader} instance used by the {@link 122 * #loadConverters()} method. 123 * 124 * <p>This field may be {@code null}.</p> 125 * 126 * @see #loadConverters() 127 * 128 * @see ServiceLoader 129 */ 130 @SuppressWarnings("rawtypes") 131 private static volatile ServiceLoader<Converter> converterLoader; 132 133 /** 134 * A {@link ServiceLoader} instance used by the {@link 135 * #loadArbiters()} method. 136 * 137 * <p>This field may be {@code null}.</p> 138 * 139 * @see #loadArbiters() 140 * 141 * @see ServiceLoader 142 */ 143 @SuppressWarnings("rawtypes") 144 private static volatile ServiceLoader<Arbiter> arbiterLoader; 145 146 /** 147 * The name of the configuration property whose value is a {@link 148 * Map} of <em>configuration coordinates</em> for the current 149 * application. 150 * 151 * <p>This field is never {@code null}.</p> 152 * 153 * <p>A request is made via the {@link #getValue(Map, String, Type)} 154 * method with {@code null} as the value of its first parameter and 155 * the value of this field as its second parameter and {@link Map 156 * Map.class} as the value of its third parameter. The returned 157 * {@link Map} is cached for the lifetime of this {@link 158 * Configurations} object and is returned by the {@link 159 * #getConfigurationCoordinates()} method.</p> 160 * 161 * @see #getValue(Map, String, Type) 162 * 163 * @see #getConfigurationCoordinates() 164 */ 165 public static final String CONFIGURATION_COORDINATES = "configurationCoordinates"; 166 167 /** 168 * An {@linkplain Collections#unmodifiableMap(Map) immutable} {@link 169 * Map} of "wrapper" {@link Class} instances indexed by their 170 * {@linkplain Class#isPrimitive() primitive} counterparts. 171 * 172 * <p>This field is never {@code null}.</p> 173 */ 174 private static final Map<Class<?>, Class<?>> wrapperTypes; 175 176 static { 177 final Map<Class<?>, Class<?>> map = new HashMap<>(); 178 map.put(boolean.class, Boolean.class); 179 map.put(byte.class, Byte.class); 180 map.put(char.class, Character.class); 181 map.put(double.class, Double.class); 182 map.put(float.class, Float.class); 183 map.put(int.class, Integer.class); 184 map.put(long.class, Long.class); 185 map.put(short.class, Short.class); 186 map.put(void.class, Void.class); 187 wrapperTypes = Collections.unmodifiableMap(map); 188 } 189 190 191 /* 192 * Instance fields. 193 */ 194 195 196 /** 197 * Whether this {@link Configurations} has been initialized. 198 * 199 * @see #Configurations(Collection, Collection, Collection) 200 */ 201 private final boolean initialized; 202 203 /** 204 * The {@link Collection} of {@link Configuration} instances that 205 * can {@linkplain Configuration#getValue(Map, String) provide} 206 * configuration values. 207 * 208 * <p>This field is never {@code null}.</p> 209 * 210 * @see #Configurations(Collection, Collection, Collection) 211 */ 212 private final Collection<Configuration> configurations; 213 214 /** 215 * The {@link Collection} of {@link Arbiter}s that can resolve 216 * otherwise ambiguous configuration values. 217 * 218 * <p>This field is never {@code null}.</p> 219 * 220 * @see #Configurations(Collection, Collection, Collection) 221 */ 222 private final Collection<Arbiter> arbiters; 223 224 /** 225 * A {@link Map} of {@link Converter} instances, indexed under 226 * {@linkplain Converter#getType() their <code>Type</code>}. 227 * 228 * <p>This field is never {@code null}.</p> 229 * 230 * @see #Configurations(Collection, Collection, Collection) 231 */ 232 private final Map<Type, Converter<?>> converters; 233 234 /** 235 * A {@link Map} representing the <em>configuration coordinates</em> 236 * of the application using this {@link Configurations}. 237 * 238 * <p>This field may be {@code null}.</p> 239 * 240 * @see #getConfigurationCoordinates() 241 */ 242 private final Map<String, String> configurationCoordinates; 243 244 /** 245 * An {@link ELContext} used for Expression Language evaluation. 246 * 247 * <p>This field is never {@code null}.</p> 248 */ 249 private final ELContext elContext; 250 251 /** 252 * An {@link ExpressionFactory} used for Expression Language 253 * evaluation. 254 * 255 * <p>This field is never {@code null}.</p> 256 */ 257 private final ExpressionFactory expressionFactory; 258 259 260 /* 261 * Constructors. 262 */ 263 264 265 /** 266 * Creates a new {@link Configurations}. 267 * 268 * <p>The {@link #loadConfigurations()}, {@link #loadConverters()} 269 * and {@link #loadArbiters()} methods will be invoked during 270 * construction.</p> 271 * 272 * @see #loadConfigurations() 273 * 274 * @see #loadConverters() 275 * 276 * @see #loadArbiters() 277 * 278 * @see #Configurations(Collection, Collection, Collection) 279 */ 280 public Configurations() { 281 this(null, null, null); 282 } 283 284 /** 285 * Creates a new {@link Configurations}. 286 * 287 * <p>The {@link #loadConverters()} and {@link #loadArbiters()} 288 * methods will be invoked during construction. IF the supplied 289 * {@code configurations} is {@code null}, then the {@link 290 * #loadConfigurations()} method will be invoked during 291 * construction.</p> 292 * 293 * @param configurations a {@link Collection} of {@link 294 * Configuration} instances; if {@code null} then the return value 295 * of the {@link #loadConfigurations()} method will be used instead 296 * 297 * @see #loadConfigurations() 298 * 299 * @see #loadConverters() 300 * 301 * @see #loadArbiters() 302 * 303 * @see #Configurations(Collection, Collection, Collection) 304 */ 305 public Configurations(final Collection<? extends Configuration> configurations) { 306 this(configurations, null, null); 307 } 308 309 /** 310 * Creates a new {@link Configurations}. 311 * 312 * <p>The {@link #loadConverters()} and {@link #loadArbiters()} 313 * methods will be invoked during construction. IF the supplied 314 * {@code configurations} is {@code null}, then the {@link 315 * #loadConfigurations()} method will be invoked during 316 * construction.</p> 317 * 318 * @param configurations a {@link Collection} of {@link 319 * Configuration} instances; if {@code null} then the return value 320 * of the {@link #loadConfigurations()} method will be used instead 321 * 322 * @param converters a {@link Collection} of {@link Converter} 323 * instances; if {@code null} then the return value of the {@link 324 * #loadConverters()} method will be used instead 325 * 326 * @param arbiters a {@link Collection} of {@link Arbiter} 327 * instances; if {@code null} then the return value of the {@link 328 * #loadArbiters()} method will be used instead 329 * 330 * @see #loadConfigurations() 331 * 332 * @see #loadConverters() 333 * 334 * @see #loadArbiters() 335 */ 336 public Configurations(Collection<? extends Configuration> configurations, 337 Collection<? extends Converter<?>> converters, 338 Collection<? extends Arbiter> arbiters) { 339 super(); 340 341 this.expressionFactory = ExpressionFactory.newInstance(); 342 assert this.expressionFactory != null; 343 final StandardELContext standardElContext = new StandardELContext(this.expressionFactory); 344 standardElContext.addELResolver(new ConfigurationELResolver()); 345 this.elContext = standardElContext; 346 347 if (configurations == null) { 348 configurations = this.loadConfigurations(); 349 } 350 if (configurations == null || configurations.isEmpty()) { 351 this.configurations = Collections.emptySet(); 352 } else { 353 this.configurations = Collections.unmodifiableCollection(new LinkedList<>(configurations)); 354 } 355 for (final Configuration configuration : configurations) { 356 if (configuration != null) { 357 configuration.setConfigurations(this); 358 } 359 } 360 361 if (converters == null) { 362 converters = this.loadConverters(); 363 } 364 if (converters == null || converters.isEmpty()) { 365 converters = Collections.emptySet(); 366 } else { 367 converters = Collections.unmodifiableCollection(new LinkedList<>(converters)); 368 } 369 assert converters != null; 370 this.converters = Collections.unmodifiableMap(converters.stream().collect(Collectors.toMap(c -> c.getType(), Function.identity()))); 371 372 if (arbiters == null) { 373 arbiters = this.loadArbiters(); 374 } 375 if (arbiters == null || arbiters.isEmpty()) { 376 this.arbiters = Collections.emptySet(); 377 } else { 378 this.arbiters = Collections.unmodifiableCollection(new LinkedList<>(arbiters)); 379 } 380 381 this.initialized = true; 382 383 final Map<String, String> coordinates = this.getValue(null, CONFIGURATION_COORDINATES, new TypeLiteral<Map<String, String>>() { 384 private static final long serialVersionUID = 1L; }.getType()); 385 if (coordinates == null || coordinates.isEmpty()) { 386 this.configurationCoordinates = Collections.emptyMap(); 387 } else { 388 this.configurationCoordinates = Collections.unmodifiableMap(coordinates); 389 } 390 391 } 392 393 394 /* 395 * Instance methods. 396 */ 397 398 399 /** 400 * Loads a {@link Collection} of {@link Configuration} objects and 401 * returns it. 402 * 403 * <p>This method never returns {@code null}.</p> 404 * 405 * <p>Overrides of this method must not return {@code null}.</p> 406 * 407 * <p>The default implementation of this method uses the {@link 408 * ServiceLoader} mechanism to load {@link Configuration} 409 * instances.</p> 410 * 411 * @return a non-{@code null}, {@link Collection} of {@link 412 * Configuration} instances 413 * 414 * @see ServiceLoader#load(Class) 415 */ 416 protected Collection<? extends Configuration> loadConfigurations() { 417 final String cn = this.getClass().getName(); 418 final String mn = "loadConfigurations"; 419 if (this.logger.isLoggable(Level.FINER)) { 420 this.logger.entering(cn, mn); 421 } 422 final Collection<Configuration> returnValue = new LinkedList<>(); 423 ServiceLoader<Configuration> configurationLoader = Configurations.configurationLoader; 424 if (configurationLoader == null) { 425 configurationLoader = ServiceLoader.load(Configuration.class); 426 assert configurationLoader != null; 427 Configurations.configurationLoader = configurationLoader; 428 } 429 final Iterator<Configuration> configurationIterator = configurationLoader.iterator(); 430 assert configurationIterator != null; 431 while (configurationIterator.hasNext()) { 432 final Configuration configuration = configurationIterator.next(); 433 assert configuration != null; 434 returnValue.add(configuration); 435 } 436 if (this.logger.isLoggable(Level.FINER)) { 437 this.logger.exiting(cn, mn, returnValue); 438 } 439 return returnValue; 440 } 441 442 /** 443 * Loads a {@link Collection} of {@link Converter} objects and 444 * returns it. 445 * 446 * <p>This method never returns {@code null}.</p> 447 * 448 * <p>Overrides of this method must not return {@code null}.</p> 449 * 450 * <p>The default implementation of this method uses the {@link 451 * ServiceLoader} mechanism to load {@link Converter} instances.</p> 452 * 453 * @return a non-{@code null}, {@link Collection} of {@link 454 * Converter} instances 455 * 456 * @see ServiceLoader#load(Class) 457 */ 458 protected Collection<? extends Converter<?>> loadConverters() { 459 final String cn = this.getClass().getName(); 460 final String mn = "loadConverters"; 461 if (this.logger.isLoggable(Level.FINER)) { 462 this.logger.entering(cn, mn); 463 } 464 final Collection<Converter<?>> returnValue = new LinkedList<>(); 465 @SuppressWarnings("rawtypes") 466 ServiceLoader<Converter> converterLoader = Configurations.converterLoader; 467 if (converterLoader == null) { 468 converterLoader = ServiceLoader.load(Converter.class); 469 assert converterLoader != null; 470 Configurations.converterLoader = converterLoader; 471 } 472 @SuppressWarnings("rawtypes") 473 final Iterator<Converter> converterIterator = converterLoader.iterator(); 474 assert converterIterator != null; 475 while (converterIterator.hasNext()) { 476 final Converter<?> converter = converterIterator.next(); 477 assert converter != null; 478 returnValue.add(converter); 479 } 480 if (this.logger.isLoggable(Level.FINER)) { 481 this.logger.exiting(cn, mn, returnValue); 482 } 483 return returnValue; 484 } 485 486 /** 487 * Loads a {@link Collection} of {@link Arbiter} objects and returns 488 * it. 489 * 490 * <p>This method never returns {@code null}.</p> 491 * 492 * <p>Overrides of this method must not return {@code null}.</p> 493 * 494 * <p>The default implementation of this method uses the {@link 495 * ServiceLoader} mechanism to load {@link Arbiter} instances.</p> 496 * 497 * @return a non-{@code null}, {@link Collection} of {@link Arbiter} 498 * instances 499 * 500 * @see ServiceLoader#load(Class) 501 */ 502 protected Collection<? extends Arbiter> loadArbiters() { 503 final String cn = this.getClass().getName(); 504 final String mn = "loadArbiters"; 505 if (this.logger.isLoggable(Level.FINER)) { 506 this.logger.entering(cn, mn); 507 } 508 final Collection<Arbiter> returnValue = new LinkedList<>(); 509 ServiceLoader<Arbiter> arbiterLoader = Configurations.arbiterLoader; 510 if (arbiterLoader == null) { 511 arbiterLoader = ServiceLoader.load(Arbiter.class); 512 assert arbiterLoader != null; 513 Configurations.arbiterLoader = arbiterLoader; 514 } 515 final Iterator<Arbiter> arbiterIterator = arbiterLoader.iterator(); 516 assert arbiterIterator != null; 517 while (arbiterIterator.hasNext()) { 518 final Arbiter arbiter = arbiterIterator.next(); 519 assert arbiter != null; 520 returnValue.add(arbiter); 521 } 522 if (this.logger.isLoggable(Level.FINER)) { 523 this.logger.exiting(cn, mn, returnValue); 524 } 525 return returnValue; 526 } 527 528 /** 529 * Returns a {@link Map} of <em>configuration 530 * coordinates</em>—aspects and their values that define a 531 * location within which requests for configuration values may take 532 * place. 533 * 534 * <p>This method may return {@code null}.</p> 535 * 536 * <p>Overrides of this method may return {@code null}.</p> 537 * 538 * <p>The default implementation of this method returns 539 * configuration coordinates that are discovered at {@linkplain 540 * #Configurations() construction time} and cached for the lifetime 541 * of this {@link Configurations} object.</p> 542 * 543 * @return a {@link Map} of configuration coordinates; may be {@code 544 * null} 545 */ 546 @Override 547 public Map<String, String> getConfigurationCoordinates() { 548 return this.configurationCoordinates; 549 } 550 551 /** 552 * Returns a non-{@code null}, {@linkplain 553 * Collections#unmodifiableSet(Set) immutable} {@link Set} of {@link 554 * Type}s representing all the types to which {@link String} 555 * configuration values may be converted by the {@linkplain 556 * #loadConverters() <code>Converter</code>s loaded} by this {@link 557 * Configurations} object. 558 * 559 * <p>This method never returns {@code null}.</p> 560 * 561 * @return a non-{@code null}, {@linkplain 562 * Collections#unmodifiableSet(Set) immutable} {@link Set} of {@link 563 * Type}s 564 */ 565 @Override 566 public final Set<Type> getConversionTypes() { 567 this.checkState(); 568 return this.converters.keySet(); 569 } 570 571 /** 572 * Returns a configuration value corresponding to the configuration 573 * property suitable for the supplied {@code 574 * configurationCoordinates} and {@code name}, or the supplied 575 * {@code defaultValue} if {@code null} would otherwise be returned, 576 * converted, if possible, to the type represented by the supplied 577 * {@code type}. 578 * 579 * <p>This method may return {@code null}.</p> 580 * 581 * @param <T> the type to which a {@link String}-typed configuration 582 * value should be converted 583 * 584 * @param configurationCoordinates a {@link Map} representing the 585 * configuration coordinates in effect for this request; may be 586 * {@code null} 587 * 588 * @param name the name of the configuration property for which a 589 * value will be returned; must not be {@code null} 590 * 591 * @param type a {@link Type} representing the type to 592 * which the configuration value will be {@linkplain 593 * Converter#convert(String) converted}; must not be {@code null} 594 * 595 * @param defaultValue the value that will be converted if {@code 596 * null} would otherwise be returned; may be {@code null} 597 * 598 * @return the configuration value, or {@code null} 599 * 600 * @exception NullPointerException if {@code name} or {@code type} 601 * is {@code null} 602 * 603 * @exception NoSuchConverterException if there is no {@link 604 * Converter} available that {@linkplain Converter#getType() 605 * handles} the {@link Type} represented by the supplied {@code 606 * type} 607 * 608 * @exception ConversionException if type conversion could not occur 609 * for any reason 610 * 611 * @exception AmbiguousConfigurationValuesException if two or more 612 * values were found that could be suitable and arbitration 613 * {@linkplain #performArbitration(Map, String, Collection) was 614 * performed} but could not resolve the dispute 615 * 616 * @exception ConfigurationException if any other 617 * configuration-related error occurs 618 * 619 * @see #getValue(Map, String, Converter, String) 620 */ 621 @Override 622 public final <T> T getValue(final Map<String, String> configurationCoordinates, final String name, Type type, final String defaultValue) { 623 final String cn = this.getClass().getName(); 624 final String mn = "getValue"; 625 if (this.logger.isLoggable(Level.FINER)) { 626 this.logger.entering(cn, mn, new Object[] { configurationCoordinates, name, type, defaultValue }); 627 } 628 if (type instanceof Class) { 629 final Class<?> c = (Class<?>)type; 630 if (c.isPrimitive()) { 631 type = wrapperTypes.get(c); 632 } 633 } 634 @SuppressWarnings("unchecked") 635 final Converter<T> converter = (Converter<T>)this.converters.get(type); 636 if (converter == null) { 637 throw new NoSuchConverterException(type); 638 } 639 if (this.logger.isLoggable(Level.FINE)) { 640 this.logger.logp(Level.FINE, cn, mn, "Using {0} to convert String to {1}", new Object[] { converter, type }); 641 } 642 final T returnValue = this.getValue(configurationCoordinates, name, converter, defaultValue); 643 if (this.logger.isLoggable(Level.FINER)) { 644 this.logger.exiting(cn, mn, returnValue); 645 } 646 return returnValue; 647 } 648 649 /** 650 * Returns a configuration value corresponding to the configuration 651 * property suitable for the supplied {@code name}, as {@linkplain 652 * Converter#convert(String) converted} by the supplied {@link 653 * Converter}. 654 * 655 * <p>This method may return {@code null}.</p> 656 * 657 * @param <T> the type to which a {@link String}-typed configuration 658 * value should be converted 659 * 660 * @param name the name of the configuration property for which a 661 * value will be returned; must not be {@code null} 662 * 663 * @param converter a {@link Converter} instance that will convert 664 * any {@link String} configuration value into the type of object 665 * that this method will return; must not be {@code null} 666 * 667 * @return the configuration value, or {@code null} 668 * 669 * @exception NullPointerException if {@code name} or {@code 670 * converter} is {@code null} 671 * 672 * @exception ConversionException if type conversion could not occur 673 * for any reason 674 * 675 * @exception AmbiguousConfigurationValuesException if two or more 676 * values were found that could be suitable and arbitration 677 * {@linkplain #performArbitration(Map, String, Collection) was 678 * performed} but could not resolve the dispute 679 * 680 * @see #getValue(Map, String, Converter, String) 681 */ 682 public final <T> T getValue(final String name, final Converter<T> converter) { 683 return this.getValue(this.getConfigurationCoordinates(), name, converter, null); 684 } 685 686 /** 687 * Returns a configuration value corresponding to the configuration 688 * property suitable for the supplied {@code 689 * configurationCoordinates} and {@code name}, as {@linkplain 690 * Converter#convert(String) converted} by the supplied {@link 691 * Converter}. 692 * 693 * <p>This method may return {@code null}.</p> 694 * 695 * @param <T> the type to which a {@link String}-typed configuration 696 * value should be converted 697 * 698 * @param configurationCoordinates a {@link Map} representing the 699 * configuration coordinates in effect for this request; may be 700 * {@code null} 701 * 702 * @param name the name of the configuration property for which a 703 * value will be returned; must not be {@code null} 704 * 705 * @param converter a {@link Converter} instance that will convert 706 * any {@link String} configuration value into the type of object 707 * that this method will return; must not be {@code null} 708 * 709 * @return the configuration value, or {@code null} 710 * 711 * @exception NullPointerException if {@code name} or {@code 712 * converter} is {@code null} 713 * 714 * @exception ConversionException if type conversion could not occur 715 * for any reason 716 * 717 * @exception AmbiguousConfigurationValuesException if two or more 718 * values were found that could be suitable and arbitration 719 * {@linkplain #performArbitration(Map, String, Collection) was 720 * performed} but could not resolve the dispute 721 * 722 * @exception ConfigurationException if any other 723 * configuration-related error occurs 724 * 725 * @see #getValue(Map, String, Converter, String) 726 */ 727 public final <T> T getValue(final Map<String, String> configurationCoordinates, final String name, final Converter<T> converter) { 728 return this.getValue(configurationCoordinates, name, converter, null); 729 } 730 731 /** 732 * Returns an object that is the value for the configuration request 733 * represented by the supplied {@code configurationCoordinates}, 734 * {@code name} and {@code defaultValue} parameters, as converted by 735 * the supplied {@link Converter}. 736 * 737 * <p>This method may return {@code null}.</p> 738 * 739 * @param <T> the type of the object to be returned 740 * 741 * @param configurationCoordinates the configuration coordinates for which 742 * a value should be selected; may be {@code null} 743 * 744 * @param name the name of the configuration property within the 745 * world defined by the supplied {@code configurationCoordinates} 746 * whose value is to be selected; must not be {@code null} 747 * 748 * @param converter a {@link Converter} instance that will convert 749 * any {@link String} configuration value into the type of object 750 * that this method will return; must not be {@code null} 751 * 752 * @param defaultValue the fallback default value to use as an 753 * absolute last resort; may be {@code null}; will also be converted 754 * by the supplied {@link Converter} 755 * 756 * @return the value for the implied configuration property, or {@code null} 757 * 758 * @exception NullPointerException if either {@code name} or {@code 759 * converter} is {@code null} 760 * 761 * @exception ConversionException if type conversion could not occur 762 * for any reason 763 * 764 * @exception AmbiguousConfigurationValuesException if two or more 765 * values were found that could be suitable and arbitration 766 * {@linkplain #performArbitration(Map, String, Collection) was 767 * performed} but could not resolve the dispute 768 * 769 * @exception ConfigurationException if any other 770 * configuration-related error occurs 771 * 772 * @see Converter#convert(String) 773 * 774 * @see Configuration#getValue(Map, String) 775 * 776 * @see #performArbitration(Map, String, Collection) 777 * 778 * @see #handleMalformedConfigurationValues(Collection) 779 */ 780 public <T> T getValue(Map<String, String> configurationCoordinates, final String name, final Converter<T> converter, final String defaultValue) { 781 final String cn = this.getClass().getName(); 782 final String mn = "getValue"; 783 if (this.logger.isLoggable(Level.FINER)) { 784 this.logger.entering(cn, mn, new Object[] { configurationCoordinates, name, converter, defaultValue }); 785 } 786 Objects.requireNonNull(name); 787 Objects.requireNonNull(converter); 788 this.checkState(); 789 if (configurationCoordinates == null) { 790 configurationCoordinates = Collections.emptyMap(); 791 } 792 793 // The selected value is the best candidate at any given moment 794 // for using to compute the return value of this method. When it 795 // is null, it means we haven't found a suitable value yet. 796 ConfigurationValue selectedValue = null; 797 798 // We use a PriorityQueue of ConfigurationValues sorted by their 799 // specificity (most specific first) to keep track of the most 800 // specific ConfigurationValue found so far. We create it only 801 // when necessary. 802 PriorityQueue<ConfigurationValue> values = null; 803 804 // Subclasses are given a chance to deal with bad values that 805 // might be encountered by badly-behaved Configuration instances; 806 // see #handleMalformedConfigurationValues(Collection) for 807 // details. 808 Collection<ConfigurationValue> badValues = null; 809 810 for (final Configuration configuration : this.configurations) { 811 assert configuration != null; 812 813 final ConfigurationValue value; 814 try { 815 if (isActive(configuration)) { 816 value = null; 817 } else { 818 this.activate(configuration); 819 value = configuration.getValue(configurationCoordinates, name); 820 } 821 } finally { 822 this.deactivate(configuration); 823 } 824 825 if (value != null) { 826 827 if (name.equals(value.getName())) { 828 Map<String, String> valueCoordinates = value.getCoordinates(); 829 if (valueCoordinates == null) { 830 valueCoordinates = Collections.emptyMap(); 831 } 832 833 final int configurationCoordinatesSize = configurationCoordinates.size(); 834 final int valueCoordinatesSize = valueCoordinates.size(); 835 836 if (configurationCoordinatesSize < valueCoordinatesSize) { 837 // Bad value! 838 if (badValues == null) { 839 badValues = new LinkedList<>(); 840 } 841 badValues.add(value); 842 843 } else if (configurationCoordinates.equals(valueCoordinates)) { 844 // We have an exact match. We hope it's going to be the 845 // only one. 846 847 if (selectedValue == null) { 848 849 if (values == null || values.isEmpty()) { 850 // There aren't any conflicts yet; this is good. This 851 // value will be our candidate. 852 selectedValue = value; 853 854 } else { 855 // We got a match, but we already *had* a match, so we 856 // don't have a candidate--instead, add it to the 857 // bucket of values that will be arbitrated later. 858 values.add(value); 859 860 } 861 862 } else { 863 assert selectedValue != null; 864 // We have an exact match, but we already identified a 865 // candidate, so oops, we have to treat our prior match 866 // and this one as non-candidates. 867 868 if (values == null) { 869 values = new PriorityQueue<>(configurationValueComparator); 870 } 871 values.add(selectedValue); 872 selectedValue = null; 873 values.add(value); 874 } 875 876 } else if (configurationCoordinatesSize == valueCoordinatesSize) { 877 // Bad value! The configuration subsystem handed back a 878 // value containing coordinates not drawn from the 879 // configurationCoordinatesSet. We know this because we 880 // already tested for Set equality, which failed, so this 881 // test means disparate entries. 882 if (badValues == null) { 883 badValues = new LinkedList<>(); 884 } 885 badValues.add(value); 886 887 } else if (selectedValue != null) { 888 // Nothing to do; we've already got our candidate. We 889 // don't break here because we're going to ensure there 890 // aren't any duplicates. 891 892 } else if (configurationCoordinates.entrySet().containsAll(valueCoordinates.entrySet())) { 893 // We specified, e.g., {a=b, c=d, e=f} and they have, say, 894 // {c=d, e=f} or {a=b, c=d} etc. but not, say, {q=r}. 895 if (values == null) { 896 values = new PriorityQueue<>(configurationValueComparator); 897 } 898 values.add(value); 899 900 } else { 901 // Bad value! 902 if (badValues == null) { 903 badValues = new LinkedList<>(); 904 } 905 badValues.add(value); 906 907 } 908 } else { 909 // We asked for "frobnicationInterval"; they responded with 910 // "hostname". Bad value. 911 if (badValues == null) { 912 badValues = new LinkedList<>(); 913 } 914 badValues.add(value); 915 } 916 } 917 } 918 assert this.allConfigurationsInactive(); 919 920 // Give a subclass a chance to deal with bad values. Dealing with 921 // them might very well involve throwing an exception which will 922 // obviously preclude arbitration and conversion. That's fine. 923 if (badValues != null && !badValues.isEmpty()) { 924 this.handleMalformedConfigurationValues(badValues); 925 } 926 927 // Perform arbitration if necessary, or otherwise ensure that we 928 // end up with the most suitable value possible. 929 if (selectedValue == null) { 930 final Collection<ConfigurationValue> valuesToArbitrate = new LinkedList<>(); 931 int highestSpecificitySoFarEncountered = -1; 932 if (values != null) { 933 VALUES_LOOP: 934 while (!values.isEmpty()) { 935 936 final ConfigurationValue value = values.poll(); 937 assert value != null; 938 939 // The values are sorted by their specificity, most specific 940 // first. 941 final int valueSpecificity = Math.max(0, value.specificity()); 942 assert highestSpecificitySoFarEncountered < 0 || valueSpecificity <= highestSpecificitySoFarEncountered; 943 944 if (highestSpecificitySoFarEncountered < 0 || valueSpecificity < highestSpecificitySoFarEncountered) { 945 if (selectedValue == null) { 946 assert valuesToArbitrate.isEmpty(); 947 selectedValue = value; 948 highestSpecificitySoFarEncountered = valueSpecificity; 949 } else if (valuesToArbitrate.isEmpty()) { 950 // We have a selected value that is non-null, and no 951 // further values to arbitrate, so we're done. We know 952 // we picked the most specific value so we effectively 953 // discard the others. 954 break VALUES_LOOP; 955 } else { 956 valuesToArbitrate.add(value); 957 } 958 } else if (valueSpecificity == highestSpecificitySoFarEncountered) { 959 assert selectedValue != null; 960 if (value.isAuthoritative()) { 961 if (selectedValue.isAuthoritative()) { 962 // Both say they're authoritative; arbitration required 963 valuesToArbitrate.add(selectedValue); 964 selectedValue = null; 965 valuesToArbitrate.add(value); 966 } else { 967 // value is authoritative; selectedValue is not; so swap 968 // them 969 selectedValue = value; 970 } 971 } else if (selectedValue.isAuthoritative()) { 972 // value is not authoritative; selected value is; so just 973 // drop value on the floor; it's not authoritative. 974 } else { 975 // Neither is authoritative; arbitration required. 976 valuesToArbitrate.add(selectedValue); 977 selectedValue = null; 978 valuesToArbitrate.add(value); 979 } 980 } else { 981 assert false : "valueSpecificity > highestSpecificitySoFarEncountered: " + valueSpecificity + " > " + highestSpecificitySoFarEncountered; 982 } 983 984 } 985 } 986 if (selectedValue == null) { 987 selectedValue = this.performArbitration(configurationCoordinates, name, Collections.unmodifiableCollection(valuesToArbitrate)); 988 } 989 } 990 991 // Perform conversion, including of null values. 992 final T returnValue; 993 if (selectedValue == null) { 994 if (defaultValue == null) { 995 returnValue = converter.convert(null); 996 } else { 997 returnValue = converter.convert(this.interpolate(defaultValue)); 998 } 999 } else { 1000 final String valueToConvert = selectedValue.getValue(); 1001 if (valueToConvert == null) { 1002 returnValue = converter.convert(null); 1003 } else { 1004 returnValue = converter.convert(this.interpolate(valueToConvert)); 1005 } 1006 } 1007 1008 if (this.logger.isLoggable(Level.FINER)) { 1009 this.logger.exiting(cn, mn, returnValue); 1010 } 1011 return returnValue; 1012 } 1013 1014 /** 1015 * Interpolates any expressions occurring within the supplied {@code 1016 * value} and returns the result of interpolation. 1017 * 1018 * <p>This method may return {@code null}.</p> 1019 * 1020 * <p>Overrides of this method may return {@code null}.</p> 1021 * 1022 * <p>The default implementation of this method performs 1023 * interpolation by using a {@link ValueExpression} as {@linkplain 1024 * ExpressionFactory#createValueExpression(ELContext, String, Class) 1025 * produced by an <code>ExpressionFactory</code>}.</p> 1026 * 1027 * <p>A {@code configurations} object is made available to any 1028 * Expression Language expressions, which exposes this {@link 1029 * Configurations} object. This means, among other things, that you 1030 * can retrieve configuration values using the following syntax:</p> 1031 * <pre>${configurations["java.home"]}</pre> 1032 * 1033 * @param value a configuration value {@link String}, before any 1034 * type conversion has taken place, with (possibly) expression 1035 * language expressions in it; may be {@code null} in which case 1036 * {@code null} is returned 1037 * 1038 * @return the result of interpolating the supplied {@code value}, 1039 * or {@code null} 1040 * 1041 * @exception PropertyNotFoundException if the supplied {@code 1042 * value} contained a valid expression language expression that 1043 * identifies an unknown property 1044 * 1045 * @see <a 1046 * href="https://docs.oracle.com/javaee/7/tutorial/jsf-el.htm#GJDDD">the 1047 * section in the Java EE Tutorial on the Unified Expression 1048 * Language</a> 1049 */ 1050 public String interpolate(final String value) { 1051 final String cn = this.getClass().getName(); 1052 final String mn = "interpolate"; 1053 if (this.logger.isLoggable(Level.FINER)) { 1054 this.logger.entering(cn, mn, value); 1055 } 1056 final String returnValue; 1057 if (value == null) { 1058 returnValue = null; 1059 } else { 1060 final ValueExpression valueExpression = this.expressionFactory.createValueExpression(this.elContext, value, String.class); 1061 assert valueExpression != null; 1062 returnValue = String.class.cast(valueExpression.getValue(this.elContext)); 1063 } 1064 if (this.logger.isLoggable(Level.FINER)) { 1065 this.logger.exiting(cn, mn, returnValue); 1066 } 1067 return returnValue; 1068 } 1069 1070 /** 1071 * Returns a {@link Set} of names of {@link ConfigurationValue}s 1072 * that might be returned by this {@link Configurations} instance. 1073 * 1074 * <p>This method does not return {@code null}.</p> 1075 * 1076 * <p>Overrides of this method must not return {@code null}.</p> 1077 * 1078 * <p>This implementation does not cache results.</p> 1079 * 1080 * <p>Just because a name appears in the returned {@link Set} does 1081 * <em>not</em> mean that a {@link ConfigurationValue} <em>will</em> 1082 * be returned for it in a location in configuration space 1083 * identified by any arbitrary set of configuration coordinates.</p> 1084 * 1085 * @return a non-{@code null} {@link Set} of names of {@link 1086 * ConfigurationValue}s 1087 */ 1088 @Override 1089 public Set<String> getNames() { 1090 final String cn = this.getClass().getName(); 1091 final String mn = "getNames"; 1092 if (this.logger.isLoggable(Level.FINER)) { 1093 this.logger.entering(cn, mn); 1094 } 1095 1096 final Set<String> returnValue; 1097 if (this.configurations == null || this.configurations.isEmpty()) { 1098 returnValue = Collections.emptySet(); 1099 } else { 1100 final Set<String> names = new TreeSet<>(); 1101 for (final Configuration configuration : this.configurations) { 1102 if (configuration != null) { 1103 final Set<String> configurationNames = configuration.getNames(); 1104 if (configurationNames != null && !configurationNames.isEmpty()) { 1105 names.addAll(configurationNames); 1106 } 1107 } 1108 } 1109 if (names.isEmpty()) { 1110 returnValue = Collections.emptySet(); 1111 } else { 1112 returnValue = Collections.unmodifiableSet(names); 1113 } 1114 } 1115 1116 if (this.logger.isLoggable(Level.FINER)) { 1117 this.logger.exiting(cn, mn, returnValue); 1118 } 1119 return returnValue; 1120 } 1121 1122 /** 1123 * Handles any badly formed {@link ConfigurationValue} instances 1124 * received from {@link Configuration} instances during the 1125 * execution of a configuration value request. 1126 * 1127 * <p>The default implementation of this method does nothing. 1128 * Malformed values are thus effectively discarded.</p> 1129 * 1130 * <p>This method is called from the {@link #getValue(Map, String, 1131 * Converter, String)} method.</p> 1132 * 1133 * @param badValues a {@link Collection} of {@link 1134 * ConfigurationValue} instances that were deemed to be malformed in 1135 * some way; may be {@code null} 1136 * 1137 * @exception ConfigurationException if the {@link #getValue(Map, 1138 * String, Converter, String)} method should abort processing 1139 * 1140 * @see #getValue(Map, String, Converter, String) 1141 */ 1142 protected void handleMalformedConfigurationValues(final Collection<ConfigurationValue> badValues) { 1143 if (this.logger.isLoggable(Level.FINER)) { 1144 final String cn = this.getClass().getName(); 1145 final String mn = "handleMalformedConfigurationValues"; 1146 this.logger.entering(cn, mn, badValues); 1147 this.logger.logp(Level.FINER, cn, mn, "Discarding {0}", badValues); 1148 this.logger.exiting(cn, mn); 1149 } 1150 } 1151 1152 /** 1153 * Given a logical request for a configuration value, represented by 1154 * the {@code configurationCoordinates} and {@code name} parameter 1155 * values, and a {@link Collection} of {@link ConfigurationValue} 1156 * instances that represents the ambiguous response from several 1157 * {@link Configuration} instances, attempts to resolve the 1158 * ambiguity by returning a single {@link ConfigurationValue} 1159 * instead. 1160 * 1161 * <p>This method may return {@code null}.</p> 1162 * 1163 * <p>Overrides of this method may return {@code null}.</p> 1164 * 1165 * <p>The default implementation of this method asks all registered 1166 * {@link Arbiter}s in turn to perform the arbitration and returns 1167 * the first non-{@code null} response received.</p> 1168 * 1169 * @param configurationCoordinates the ({@linkplain 1170 * Collections#unmodifiableMap(Map) immutable}) configuration 1171 * coordinates in effect for the request; may be {@code null} 1172 * 1173 * @param name the name of the configuration value; may be {@code 1174 * null} 1175 * 1176 * @param values an {@linkplain 1177 * Collections#unmodifiableCollection(Collection) immutable} {@link 1178 * Collection} of definitionally ambiguous {@link 1179 * ConfigurationValue}s that resulted from the request; may be 1180 * {@code null} 1181 * 1182 * @return the result of arbitration, or {@code null} 1183 * 1184 * @exception AmbiguousConfigurationValuesException if successful 1185 * arbitration did not happen for any reason 1186 * 1187 * @see Arbiter 1188 */ 1189 protected ConfigurationValue performArbitration(final Map<? extends String, ? extends String> configurationCoordinates, 1190 final String name, 1191 final Collection<? extends ConfigurationValue> values) { 1192 final String cn = this.getClass().getName(); 1193 final String mn = "performArbitration"; 1194 if (this.logger.isLoggable(Level.FINER)) { 1195 this.logger.entering(cn, mn, new Object[] { configurationCoordinates, name, values }); 1196 } 1197 1198 ConfigurationValue returnValue = null; 1199 if (this.arbiters != null && !this.arbiters.isEmpty()) { 1200 for (final Arbiter arbiter : arbiters) { 1201 if (arbiter != null) { 1202 final ConfigurationValue arbitrationResult = arbiter.arbitrate(configurationCoordinates, name, values); 1203 if (arbitrationResult != null) { 1204 returnValue = arbitrationResult; 1205 break; 1206 } 1207 } 1208 } 1209 } 1210 if (returnValue == null && values != null && !values.isEmpty()) { 1211 throw new AmbiguousConfigurationValuesException(null, null, configurationCoordinates, name, values); 1212 } 1213 1214 if (this.logger.isLoggable(Level.FINER)) { 1215 this.logger.exiting(cn, mn, returnValue); 1216 } 1217 return returnValue; 1218 } 1219 1220 /** 1221 * If this {@link Configurations} has not yet finished {@linkplain 1222 * #Configurations() constructing}, then this method will throw an 1223 * {@link IllegalStateException}. 1224 * 1225 * @exception IllegalStateException if this {@link Configurations} 1226 * has not yet finished {@linkplain #Configurations() constructing} 1227 * 1228 * @see #Configurations() 1229 */ 1230 private final void checkState() { 1231 if (!this.initialized) { 1232 throw new IllegalStateException(); 1233 } 1234 } 1235 1236 /** 1237 * Returns {@code true} if the supplied {@link Configuration} is 1238 * currently in the middle of executing its {@link 1239 * Configuration#getValue(Map, String)} method on the current {@link 1240 * Thread}. 1241 * 1242 * @param configuration the {@link Configuration} to test; may be 1243 * {@code null} in which case {@code false} will be returned 1244 * 1245 * @return {@code true} if the supplied {@link Configuration} is 1246 * active; {@code false} otherwise 1247 * 1248 * @see #activate(Configuration) 1249 * 1250 * @see #deactivate(Configuration) 1251 */ 1252 private final boolean isActive(final Configuration configuration) { 1253 final boolean returnValue; 1254 if (configuration == null) { 1255 returnValue = false; 1256 } else { 1257 final Set<Configuration> configurations = staticCurrentlyActiveConfigurations.get().get(this); 1258 returnValue = configurations != null && configurations.contains(configuration); 1259 } 1260 return returnValue; 1261 } 1262 1263 /** 1264 * Records that the supplied {@link Configuration} is in the middle 1265 * of executing its {@link Configuration#getValue(Map, String)} 1266 * method on the current {@link Thread}. 1267 * 1268 * <p>This method is idempotent.</p> 1269 * 1270 * @param configuration the {@link Configuration} in question; may 1271 * be {@code null} in which case no action is taken 1272 * 1273 * @see #isActive(Configuration) 1274 * 1275 * @see #deactivate(Configuration) 1276 */ 1277 private final void activate(final Configuration configuration) { 1278 if (configuration != null) { 1279 final Map<Configurations, Set<Configuration>> map = staticCurrentlyActiveConfigurations.get(); 1280 assert map != null; 1281 Set<Configuration> configurations = map.get(this); 1282 if (configurations == null) { 1283 configurations = new HashSet<>(); 1284 map.put(this, configurations); 1285 } 1286 configurations.add(configuration); 1287 assert this.isActive(configuration); 1288 } 1289 } 1290 1291 /** 1292 * Records that the supplied {@link Configuration} is no longer in 1293 * the middle of executing its {@link Configuration#getValue(Map, 1294 * String)} method on the current {@link Thread}. 1295 * 1296 * <p>This method is idempotent.</p> 1297 * 1298 * @param configuration the {@link Configuration} in question; may 1299 * be {@code null} in which case no action is taken 1300 * 1301 * @see #isActive(Configuration) 1302 * 1303 * @see #activate(Configuration) 1304 */ 1305 private final void deactivate(final Configuration configuration) { 1306 if (configuration != null) { 1307 final Set<Configuration> configurations = staticCurrentlyActiveConfigurations.get().get(this); 1308 if (configurations != null) { 1309 configurations.remove(configuration); 1310 } 1311 } 1312 assert !this.isActive(configuration); 1313 } 1314 1315 /** 1316 * Returns {@code true} if all {@link Configuration} instances have 1317 * been {@linkplain #deactivate(Configuration) deactivated}. 1318 * 1319 * @return {@code true} if all {@link Configuration} instances have 1320 * been {@linkplain #deactivate(Configuration) deactivated}; {@code 1321 * false} otherwise 1322 * 1323 * @see #isActive(Configuration) 1324 * 1325 * @see #deactivate(Configuration) 1326 */ 1327 private final boolean allConfigurationsInactive() { 1328 final Set<Configuration> configurations = staticCurrentlyActiveConfigurations.get().get(this); 1329 return configurations == null || configurations.isEmpty(); 1330 } 1331 1332 1333 /* 1334 * Inner and nested classes. 1335 */ 1336 1337 1338 /** 1339 * An {@link ELResolver} that resolves a {@code configurations} 1340 * top-level object in the Expression Language and resolves its 1341 * properties by treating them as the names of configuration 1342 * properties. 1343 * 1344 * @author <a href="https://about.me/lairdnelson" 1345 * target="_parent">Laird Nelson</a> 1346 * 1347 * @see ELResolver 1348 * 1349 * @see StandardELContext#addELResolver(ELResolver) 1350 */ 1351 private final class ConfigurationELResolver extends ELResolver { 1352 1353 1354 /* 1355 * Constructors. 1356 */ 1357 1358 1359 /** 1360 * Creates a new {@link ConfigurationELResolver}. 1361 */ 1362 private ConfigurationELResolver() { 1363 super(); 1364 } 1365 1366 1367 /* 1368 * Instance methods. 1369 */ 1370 1371 1372 /** 1373 * Returns {@link Class Object.class} when invoked. 1374 * 1375 * @param elContext the {@link ELContext} in effect; ignored 1376 * 1377 * @param base the base {@link Object} in effect; ignored 1378 * 1379 * @return {@link Class Object.class} when invoked 1380 * 1381 * @see ELResolver#getCommonPropertyType(ELContext, Object) 1382 */ 1383 @Override 1384 public final Class<?> getCommonPropertyType(final ELContext elContext, final Object base) { 1385 return Object.class; 1386 } 1387 1388 /** 1389 * Returns {@code null} when invoked. 1390 * 1391 * @param elContext the {@link ELContext} in effect; ignored 1392 * 1393 * @param base the base {@link Object} in effect; ignored 1394 * 1395 * @return {@code null} when invoked 1396 * 1397 * @see ELResolver#getFeatureDescriptors(ELContext, Object) 1398 */ 1399 @Override 1400 public final Iterator<FeatureDescriptor> getFeatureDescriptors(final ELContext elContext, final Object base) { 1401 return null; 1402 } 1403 1404 /** 1405 * Calls {@link ELContext#setPropertyResolved(boolean) 1406 * elContext.setPropertyResolved(true)} and returns {@code true} 1407 * when invoked. 1408 * 1409 * @param elContext the {@link ELContext} in effect; ignored 1410 * 1411 * @param base the base {@link Object} in effect; ignored 1412 * 1413 * @param property the property in effect; ignored 1414 * 1415 * @return {@code true} when invoked 1416 * 1417 * @see ELContext#setPropertyResolved(boolean) 1418 * 1419 * @see ELResolver#isReadOnly(ELContext, Object, Object) 1420 */ 1421 @Override 1422 public final boolean isReadOnly(final ELContext elContext, final Object base, final Object property) { 1423 if (elContext != null && (property instanceof String || property instanceof Configurations)) { 1424 elContext.setPropertyResolved(true); 1425 } 1426 return true; 1427 } 1428 1429 /** 1430 * Returns a {@link Class} representing the type of object that 1431 * the supplied {@code base}/{@code property} pair represents. 1432 * 1433 * <p>This method may return {@code null}.</p> 1434 * 1435 * <p>If the supplied {@code base} is {@code null} and the 1436 * supplied {@code property} is a {@link String} equal to "{@code 1437 * configurations}", then this method returns {@link Class 1438 * Configurations.class}.</p> 1439 * 1440 * <p>If the supplied {@code base} is an instance of {@link 1441 * Configurations} and the supplied {@code property} is a {@link 1442 * String}, then the {@code property} is treated as the name of a 1443 * configuration property, and the {@link 1444 * Configurations#getValue(String)} method is invoked on the 1445 * {@code base} object with it. If the return value of that 1446 * method invocation is {@code null}, then a {@link 1447 * PropertyNotFoundException} is thrown; otherwise {@link Class 1448 * String.class} is returned.</p> 1449 * 1450 * <p>This method returns {@code null} in all other cases.</p> 1451 * 1452 * @param elContext the {@link ELContext} in effect; must not be 1453 * {@code null} 1454 * 1455 * @param base the base; may be {@code null} 1456 * 1457 * @param property the property; may be {@code null} 1458 * 1459 * @return {@link Class String.class} or {@code null} 1460 * 1461 * @exception NullPointerException if {@code elContext} is {@code 1462 * null} 1463 * 1464 * @exception PropertyNotFoundException if {@code property} is a 1465 * {@link String} but does not identify a configuration property 1466 * that has a value 1467 * 1468 * @see ELResolver#getType(ELContext, Object, Object) 1469 * 1470 * @see Configurations#getValue(String) 1471 */ 1472 @Override 1473 public final Class<?> getType(final ELContext elContext, final Object base, final Object property) { 1474 Objects.requireNonNull(elContext); 1475 Class<?> returnValue = null; 1476 if (base == null) { 1477 if ("configurations".equals(property)) { 1478 elContext.setPropertyResolved(true); 1479 returnValue = Configurations.class; 1480 } 1481 } else if (base instanceof Configurations) { 1482 if (property instanceof String) { 1483 final String value = ((Configurations)base).getValue(property.toString()); 1484 elContext.setPropertyResolved(true); 1485 if (value == null) { 1486 throw new PropertyNotFoundException(property.toString()); 1487 } 1488 returnValue = String.class; 1489 } 1490 } 1491 return returnValue; 1492 } 1493 1494 /** 1495 * Returns the proper value for the supplied {@code base}/{@code 1496 * property} pair. 1497 * 1498 * <p>This method may return {@code null}.</p> 1499 * 1500 * <p>If the supplied {@code base} is {@code null} and the 1501 * supplied {@code property} is a {@link String} equal to "{@code 1502 * configurations}", then this method returns the {@link 1503 * Configurations} object housing this {@link ELResolver} 1504 * implementation.</p> 1505 * 1506 * <p>If the supplied {@code base} is an instance of {@link 1507 * Configurations} and the supplied {@code property} is a {@link 1508 * String}, then the {@code property} is treated as the name of a 1509 * configuration property, and the {@link 1510 * Configurations#getValue(String)} method is invoked on the 1511 * {@code base} object with it. If the return value of that 1512 * method invocation is {@code null}, then a {@link 1513 * PropertyNotFoundException} is thrown; otherwise the return 1514 * value of its {@link ConfigurationValue#getValue()} method is 1515 * returned.</p> 1516 * 1517 * <p>This method returns {@code null} in all other cases.</p> 1518 * 1519 * @param elContext the {@link ELContext} in effect; must not be 1520 * {@code null} 1521 * 1522 * @param base the base; may be {@code null} 1523 * 1524 * @param property the property; may be {@code null} 1525 * 1526 * @return a {@link Configurations} object, a {@link String} or 1527 * {@code null} 1528 * 1529 * @exception NullPointerException if {@code elContext} is {@code 1530 * null} 1531 * 1532 * @exception PropertyNotFoundException if {@code property} is a 1533 * {@link String} but does not identify a configuration property 1534 * that has a value 1535 * 1536 * @see ELResolver#getValue(ELContext, Object, Object) 1537 * 1538 * @see Configurations#getValue(String) 1539 */ 1540 @Override 1541 public final Object getValue(final ELContext elContext, final Object base, final Object property) { 1542 Objects.requireNonNull(elContext); 1543 Object returnValue = null; 1544 if (base == null) { 1545 if ("configurations".equals(property)) { 1546 elContext.setPropertyResolved(true); 1547 returnValue = Configurations.this; 1548 } 1549 } else if (base instanceof Configurations) { 1550 if (property instanceof String) { 1551 returnValue = ((Configurations)base).getValue(property.toString()); 1552 elContext.setPropertyResolved(true); 1553 if (returnValue == null) { 1554 throw new PropertyNotFoundException(property.toString()); 1555 } 1556 } 1557 } 1558 return returnValue; 1559 } 1560 1561 /** 1562 * Effectively does nothing by {@linkplain 1563 * ELContext#setPropertyResolved(boolean) marking the supplied 1564 * <code>property</code> as not resolved}. 1565 * 1566 * @param elContext the {@link ELContext} in effect; may be {@code null} 1567 * 1568 * @param base the base; ignored; may be {@code null} 1569 * 1570 * @param property the property; ignored; may be {@code null} 1571 * 1572 * @param value the value; ignored; may be {@code null} 1573 * 1574 * @see ELResolver#setValue(ELContext, Object, Object, Object) 1575 * 1576 * @see #isReadOnly(ELContext, Object, Object) 1577 */ 1578 @Override 1579 public final void setValue(final ELContext elContext, final Object base, final Object property, final Object value) { 1580 if (elContext != null) { 1581 elContext.setPropertyResolved(false); 1582 } 1583 } 1584 1585 } 1586 1587}