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}