001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2018–2021 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.microprofile.config; 018 019import java.lang.ref.ReferenceQueue; 020import java.lang.ref.WeakReference; 021 022import java.security.AccessController; 023import java.security.PrivilegedAction; 024 025import java.util.Collection; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.Map.Entry; 029import java.util.NoSuchElementException; 030import java.util.Objects; 031import java.util.WeakHashMap; 032 033import org.eclipse.microprofile.config.Config; 034import org.eclipse.microprofile.config.ConfigProvider; // for javadoc only 035 036/** 037 * An {@link AutoCloseable} implementation of the abstract {@link 038 * org.eclipse.microprofile.config.spi.ConfigProviderResolver} class. 039 * 040 * <h2>Thread Safety</h2> 041 * 042 * <p>This class is safe for concurrent use by multiple threads.</p> 043 * 044 * @author <a href="https://about.me/lairdnelson" 045 * target="_parent">Laird Nelson</a> 046 * 047 * @see org.eclipse.microprofile.config.spi.ConfigProviderResolver 048 */ 049public final class ConfigProviderResolver extends org.eclipse.microprofile.config.spi.ConfigProviderResolver implements AutoCloseable { 050 051 // @GuardedBy("self") 052 private final WeakHashMap<ClassLoader, WeakAutoCloseableReference<ClassLoader, Config>> configMap; 053 054 private final ReferenceQueue<? super ClassLoader> autoClosingReferenceQueue; 055 056 /** 057 * Creates a new {@link ConfigProviderResolver}. 058 */ 059 public ConfigProviderResolver() { 060 super(); 061 this.configMap = new WeakHashMap<>(); 062 this.autoClosingReferenceQueue = new ReferenceQueue<>(); 063 final Thread cleaner = new AutoCloseableCloserThread(this.autoClosingReferenceQueue); 064 cleaner.start(); 065 } 066 067 /** 068 * Closes this {@link ConfigProviderResolver} using a best-effort 069 * strategy. 070 * 071 * <p>This method attempts to {@linkplain #releaseConfig(Config) 072 * release} each of the {@link AutoCloseable} {@link Config} 073 * instances that are {@linkplain #registerConfig(Config, 074 * ClassLoader) registered} with it. If any exception occurs, it is 075 * aggregated with any others and thrown at the end of the whole 076 * process.</p> 077 * 078 * <h2>Thread Safety</h2> 079 * 080 * <p>This method is idempotent and safe for concurrent use by 081 * multiple threads.</p> 082 * 083 * @exception RuntimeException if an error occurs 084 * 085 * @see #registerConfig(Config, ClassLoader) 086 * 087 * @see #releaseConfig(Config) 088 */ 089 @Override 090 public final void close() { 091 synchronized (this.configMap) { 092 if (!this.configMap.isEmpty()) { 093 // Remember that configMap can technically be shrinking all 094 // the time, even with the lock held. 095 final Collection<? extends WeakAutoCloseableReference<?, ? extends Config>> configsSnapshot = new HashSet<>(this.configMap.values()); 096 if (!configsSnapshot.isEmpty()) { 097 RuntimeException throwMe = null; 098 for (final WeakAutoCloseableReference<?, ? extends Config> weakConfigReference : configsSnapshot) { 099 assert weakConfigReference != null; 100 final Config config = weakConfigReference.getPotentialAutoCloseable(); 101 if (config != null) { 102 try { 103 this.releaseConfig(config); 104 } catch (final RuntimeException runtimeException) { 105 if (throwMe == null) { 106 throwMe = runtimeException; 107 } else { 108 throwMe.addSuppressed(runtimeException); 109 } 110 } catch (final Exception exception) { 111 if (throwMe == null) { 112 throwMe = new RuntimeException(exception); 113 } else { 114 throwMe.addSuppressed(exception); 115 } 116 } 117 } 118 } 119 if (throwMe != null) { 120 throw throwMe; 121 } 122 } 123 } 124 } 125 } 126 127 /** 128 * Creates and returns a new {@link 129 * org.eclipse.microprofile.config.spi.ConfigBuilder}. 130 * 131 * <p>This method never returns {@code null}.</p> 132 * 133 * <h2>Thread Safety</h2> 134 * 135 * <p>This method is safe for concurrent use by multiple threads.</p> 136 * 137 * <p>The {@link ConfigBuilder} implementation is safe for 138 * concurrent use by multiple threads.</p> 139 * 140 * <h2>Implementation Notes</h2> 141 * 142 * <p>Because of the requirement of having an implementation of the 143 * {@link ConfigBuilder#forClassLoader(ClassLoader)} method, the 144 * {@link ConfigBuilder} implementation returned by this method may 145 * contain a strong reference to a {@link ClassLoader} supplied to 146 * it by that method. The programmer needs to take care that this 147 * does not result in classloader leaks. In general, {@link 148 * ConfigBuilder}s should be retained for only as long as is 149 * necessary to {@linkplain ConfigBuilder#build() build} a {@link 150 * Config} and no longer.</p> 151 * 152 * @return a new {@link 153 * org.eclipse.microprofile.config.spi.ConfigBuilder}; never {@code 154 * null} 155 */ 156 @Override 157 public final org.eclipse.microprofile.config.spi.ConfigBuilder getBuilder() { 158 return new ConfigBuilder(); 159 } 160 161 /** 162 * Returns the sole {@link Config} instance appropriate for the 163 * {@linkplain Thread#getContextClassLoader() context 164 * <code>ClassLoader</code>}, creating and building a default one 165 * just in time if necessary. 166 * 167 * <h2>Thread Safety</h2> 168 * 169 * <p>This method is safe for concurrent use by multiple threads.</p> 170 * 171 * <p>This method never returns {@code null}.</p> 172 * 173 * @return a {@link Config} instance; never {@code null} 174 * 175 * @see #getConfig(ClassLoader) 176 * 177 * @see Thread#getContextClassLoader() 178 * 179 * @see 180 * org.eclipse.microprofile.config.ConfigProvider#getConfig() 181 */ 182 @Override 183 public final Config getConfig() { 184 return this.getConfig(AccessController.doPrivileged((PrivilegedAction<ClassLoader>)() -> Thread.currentThread().getContextClassLoader())); 185 } 186 187 /** 188 * Returns the sole {@link Config} instance appropriate for the 189 * supplied {@link ClassLoader}, creating and building a default one 190 * just in time if necessary. 191 * 192 * <h2>Thread Safety</h2> 193 * 194 * <p>This method is safe for concurrent use by multiple threads.</p> 195 * 196 * <h2>Implementation Notes</h2> 197 * 198 * <p>The specification does not indicate what to do if the supplied 199 * {@link ClassLoader} is {@code null}, but spread throughout other 200 * areas of the specification {@linkplain ConfigProvider#getConfig() 201 * there are implications} that a {@code null} {@link ClassLoader} 202 * means that the current thread's {@linkplain 203 * Thread#getContextClassLoader() context classloader} should be 204 * used instead. This implementation follows those implications. 205 * See <a 206 * href="https://github.com/eclipse/microprofile-config/issues/426" 207 * target="_parent">issue 426</a> for more details. 208 * 209 * @param classLoader the {@link ClassLoader} used to identify the 210 * {@link Config} to return; may be {@code null} in which case the 211 * {@linkplain Thread#getContextClassLoader() context 212 * <code>ClassLoader</code>} will be used instead 213 * 214 * @return a {@link Config} instance; never {@code null} 215 * 216 * @see #getBuilder() 217 * 218 * @see #getConfig() 219 * 220 * @see <a 221 * href="https://github.com/eclipse/microprofile-config/issues/426" 222 * target="_parent">MicroProfile Config issue 426</a> 223 */ 224 @Override 225 public final Config getConfig(ClassLoader classLoader) { 226 if (classLoader == null) { 227 // See https://github.com/eclipse/microprofile-config/issues/426 228 classLoader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)() -> Thread.currentThread().getContextClassLoader()); 229 } 230 Config returnValue = null; 231 synchronized (this.configMap) { 232 final WeakAutoCloseableReference<?, ? extends Config> configReference = this.configMap.get(classLoader); 233 if (configReference != null) { 234 returnValue = configReference.getPotentialAutoCloseable(); 235 } 236 if (returnValue == null) { 237 returnValue = this.getBuilder() 238 .addDefaultSources() 239 .addDiscoveredSources() 240 .addDiscoveredConverters() 241 .forClassLoader(classLoader) 242 .build(); 243 assert returnValue != null; 244 // Deliberately called with the lock held on the configMap 245 this.registerConfig(returnValue, classLoader); 246 } 247 } 248 return returnValue; 249 } 250 251 /** 252 * Registers the supplied {@link Config} instance under the supplied 253 * {@link ClassLoader} in some way. 254 * 255 * <h2>Thread Safety</h2> 256 * 257 * <p>This method is safe for concurrent use by multiple threads.</p> 258 * 259 * <h2>Implementation Notes</h2> 260 * 261 * <p>The specification says:</p> 262 * 263 * <blockquote>If the ClassLoader is null then the current 264 * Application will be used.</blockquote> 265 * 266 * <p>This implementation assumes "current Application" is 267 * equivalent to "current thread's {@linkplain 268 * Thread#getContextClassLoader() context 269 * <code>ClassLoader</code>}". See <a 270 * href="https://github.com/eclipse/microprofile-config/issues/426" 271 * target="_parent">issue 426</a> for more details.</p> 272 * 273 * @param config the {@link Config} to register; may be {@code null} 274 * in which case no action will be taken 275 * 276 * @param classLoader the {@link ClassLoader} to use as the key 277 * under which the supplied {@link Config} should be registered; if 278 * {@code null} then the return value of {@link 279 * Thread#getContextClassLoader()} will be used instead 280 * 281 * @see 282 * org.eclipse.microprofile.config.spi.ConfigProviderResolver#registerConfig(Config, 283 * ClassLoader) 284 * 285 * @see <a 286 * href="https://github.com/eclipse/microprofile-config/issues/426" 287 * target="_parent">MicroProfile Config issue 426</a> 288 */ 289 @Override 290 public final void registerConfig(final Config config, ClassLoader classLoader) { 291 if (config != null) { 292 if (classLoader == null) { 293 // See https://github.com/eclipse/microprofile-config/issues/426 294 classLoader = AccessController.doPrivileged((PrivilegedAction<ClassLoader>)() -> Thread.currentThread().getContextClassLoader()); 295 } 296 final WeakAutoCloseableReference<ClassLoader, Config> configReference = new WeakAutoCloseableReference<>(classLoader, config, this.autoClosingReferenceQueue); 297 synchronized (this.configMap) { 298 final WeakAutoCloseableReference<?, ? extends Config> oldConfigReference = this.configMap.putIfAbsent(classLoader, configReference); 299 if (oldConfigReference != null) { 300 final Config oldConfig = oldConfigReference.getPotentialAutoCloseable(); 301 if (oldConfig != null) { 302 // The specification says that an IllegalStateException 303 // should be thrown "if there is already a Config registered 304 // within the Application." It is not entirely clear what 305 // "within the Application" means. We do the best we can 306 // here. 307 throw new IllegalStateException("configMap.containsKey(" + classLoader + "): " + oldConfig); 308 } 309 } 310 } 311 } 312 } 313 314 /** 315 * Releases the supplied {@link Config} by {@linkplain 316 * AutoCloseable#close() closing} it if it implements {@link 317 * AutoCloseable} and atomically removes all of its {@linkplain 318 * #registerConfig(Config, ClassLoader) registrations}. 319 * 320 * <p>An {@link AutoCloseable} {@link Config} implementation 321 * released by this method becomes effectively unusable by design 322 * after this method completes.</p> 323 * 324 * <p>In general, owing to the underspecified nature of this method, 325 * end users should probably not call it directly.</p> 326 * 327 * <h2>Thread Safety</h2> 328 * 329 * <p>This method is safe for concurrent use by multiple threads.</p> 330 * 331 * <h2>Implementation Notes</h2> 332 * 333 * <p>The specification says:</p> 334 * 335 * <blockquote>A Config normally gets released if the Application it 336 * is associated with gets destroyed.</blockquote> 337 * 338 * <p>This implementation assumes the previous sentence means that 339 * if a {@link Config} was previously {@linkplain 340 * #registerConfig(Config, ClassLoader) registered} under a {@link 341 * ClassLoader} <em>A</em>, and under a {@link ClassLoader} 342 * <em>B</em>, then both registrations will be deleted. The 343 * equivalence between "Application" and {@link ClassLoader} is 344 * derived from the documentation for the {@link 345 * ConfigProvider#getConfig()} method, where there is an implication 346 * that the {@linkplain Thread#getContextClassLoader() context 347 * classloader} will be used as the key under which the {@link 348 * Config} to be returned may be found.</p> 349 * 350 * <p>{@linkplain ConfigProvider#getConfig(ClassLoader) Elsewhere in 351 * the specification}, it is stated:</p> 352 * 353 * <blockquote>There is exactly a single Config instance per 354 * ClassLoader[.]</blockquote> 355 * 356 * <p>This is of course false since there may be zero {@link Config} 357 * instances per {@link ClassLoader}. Leaving that aside, this 358 * sentence also does not say whether it is permitted for a single 359 * {@link Config} instance to be shared between two {@link 360 * ClassLoader}s. This implementation permits such perhaps edge 361 * cases, but a call to this {@link #releaseConfig(Config)} method 362 * will remove both such registrations if present.</p> 363 * 364 * <p>The specification has no guidance on what to do if a {@link 365 * Config} is released, and then another one is acquired via {@link 366 * ConfigProvider#getConfig(ClassLoader)}, if anything. This 367 * implementation does not track {@link Config} instances once they 368 * have been released.</p> 369 * 370 * <p>The specification does not indicate whether the supplied 371 * {@link Config} must be non-{@code null}. This implementation 372 * consequently accepts {@code null} {@link Config}s and does 373 * nothing in such cases.</p> 374 * 375 * <p>This method is called by the {@link #close()} method. 376 * 377 * @param config the {@link Config} to release; may be {@code null} 378 * in which case no action will be taken 379 * 380 * @see #close() 381 * 382 * @see #registerConfig(Config, ClassLoader) 383 */ 384 @Override 385 public final void releaseConfig(final Config config) { 386 // The specification says nothing about whether arguments can be null. 387 if (config != null) { 388 RuntimeException throwMe = null; 389 synchronized (this.configMap) { 390 if (!this.configMap.isEmpty()) { 391 final Collection<? extends Entry<?, ? extends WeakAutoCloseableReference<?, ? extends Config>>> entrySet = this.configMap.entrySet(); 392 if (entrySet != null && !entrySet.isEmpty()) { 393 final Iterator<? extends Entry<?, ? extends WeakAutoCloseableReference<?, ? extends Config>>> entryIterator = entrySet.iterator(); 394 if (entryIterator != null) { 395 while (entryIterator.hasNext()) { 396 Entry<?, ? extends WeakAutoCloseableReference<?, ? extends Config>> entry = null; 397 try { 398 entry = entryIterator.next(); 399 } catch (final NoSuchElementException noSuchElementException) { 400 // This can happen legally because we're working 401 // with a WeakHashMap, so keys are effectively being 402 // removed by the garbage collector at any point. 403 } 404 if (entry != null) { 405 final WeakAutoCloseableReference<?, ? extends Config> configReference = entry.getValue(); 406 if (configReference != null) { 407 final Config existingConfig = configReference.getPotentialAutoCloseable(); 408 if (config.equals(existingConfig)) { 409 try { 410 entryIterator.remove(); 411 } catch (final RuntimeException runtimeException) { 412 if (throwMe == null) { 413 throwMe = runtimeException; 414 } else { 415 throwMe.addSuppressed(runtimeException); 416 } 417 } 418 if (existingConfig instanceof AutoCloseable) { 419 try { 420 ((AutoCloseable)existingConfig).close(); 421 } catch (final RuntimeException runtimeException) { 422 if (throwMe == null) { 423 throwMe = runtimeException; 424 } else { 425 throwMe.addSuppressed(runtimeException); 426 } 427 } catch (final InterruptedException interruptedException) { 428 Thread.currentThread().interrupt(); 429 } catch (final Exception exception) { 430 if (throwMe == null) { 431 throwMe = new RuntimeException(exception.getMessage(), exception); 432 } else { 433 throwMe.addSuppressed(exception); 434 } 435 } 436 } 437 } 438 } 439 } 440 } 441 } 442 } 443 } 444 } 445 if (throwMe != null) { 446 throw throwMe; 447 } 448 } 449 } 450 451 @SuppressWarnings("try") 452 private static final class WeakAutoCloseableReference<R, A> extends WeakReference<R> implements AutoCloseable { 453 454 private final A potentialAutoCloseable; 455 456 private WeakAutoCloseableReference(final R referent, 457 final A potentialAutoCloseable, 458 final ReferenceQueue<? super R> referenceQueue) { 459 super(referent, Objects.requireNonNull(referenceQueue)); 460 this.potentialAutoCloseable = potentialAutoCloseable; 461 } 462 463 private final A getPotentialAutoCloseable() { 464 return this.potentialAutoCloseable; 465 } 466 467 @Override 468 public final void close() throws Exception { 469 final A potentialAutoCloseable = this.getPotentialAutoCloseable(); 470 if (potentialAutoCloseable instanceof AutoCloseable) { 471 ((AutoCloseable)potentialAutoCloseable).close(); 472 } 473 } 474 475 } 476 477 private static final class AutoCloseableCloserThread extends Thread { 478 479 private final ReferenceQueue<?> referenceQueue; 480 481 private AutoCloseableCloserThread(final ReferenceQueue<?> referenceQueue) { 482 super(AutoCloseableCloserThread.class.getName()); 483 this.setDaemon(true); 484 this.setPriority(Thread.MIN_PRIORITY + 1); // low but not too low 485 this.referenceQueue = Objects.requireNonNull(referenceQueue); 486 } 487 488 @Override 489 public final void run() { 490 while (!this.isInterrupted()) { 491 try { 492 final Object reference = this.referenceQueue.remove(); 493 if (reference instanceof AutoCloseable) { 494 try { 495 ((AutoCloseable)reference).close(); 496 } catch (final InterruptedException interruptedException) { 497 this.interrupt(); 498 } catch (final RuntimeException throwMe) { 499 throw throwMe; 500 } catch (final Exception exception) { 501 throw new RuntimeException(exception.getMessage(), exception); 502 } 503 } 504 } catch (final InterruptedException interruptedException) { 505 this.interrupt(); 506 } catch (final RuntimeException throwMe) { 507 throw throwMe; 508 } catch (final Exception exception) { 509 throw new RuntimeException(exception.getMessage(), exception); 510 } 511 } 512 } 513 514 } 515 516}