All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.microbean.microprofile.config.Config Maven / Gradle / Ivy

The newest version!
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
 *
 * Copyright © 2018–2021 microBean™.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package org.microbean.microprofile.config;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;

import java.lang.reflect.Type;

import java.net.URL;

import java.nio.charset.StandardCharsets;

import java.security.AccessController;
import java.security.PrivilegedAction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator; // for javadoc only
import java.util.List;
import java.util.LinkedList;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeSet;

import org.eclipse.microprofile.config.spi.ConfigSource;
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
import org.eclipse.microprofile.config.spi.Converter;

/**
 * A {@link Serializable} implementation of the {@link
 * org.eclipse.microprofile.config.Config} interface that is also
 * a {@link Closeable} {@link TypeConverter}.
 *
 * @author Laird Nelson
 *
 * @see org.eclipse.microprofile.config.Config
 */
public final class Config implements Closeable, org.eclipse.microprofile.config.Config, Serializable, TypeConverter {

  private static final long serialVersionUID = 1L;

  private final TypeConverter typeConverter;

  private final Collection sources;

  private volatile boolean closed;

  /**
   * Creates a new {@link Config} with a default set of {@link ConfigSource}s and a
   * {@linkplain Converter default set of Converters}
   * (including discovered {@link Converter}s).
   *
   * @exception IOException if an error occurs while reading a
   * {@code META-INF/microprofile-config.properties} resource
   *
   * @exception java.util.ServiceConfigurationError if there is a
   * problem interacting with a {@link ServiceLoader}
   */
  public Config() throws IOException {
    super();
    final List sources = new LinkedList<>();
    final Collection defaultConfigSources = getDefaultConfigSources();
    final Collection discoveredConfigSources = getDiscoveredConfigSources(null);
    sources.addAll(defaultConfigSources);
    sources.addAll(discoveredConfigSources);
    Collections.sort(sources, ConfigSourceComparator.INSTANCE);
    this.sources = Collections.unmodifiableCollection(sources);
    this.typeConverter = new ConversionHub();
  }

  /**
   * Creates a new {@link Config} instance.
   *
   * 

The MicroProfile Config specification wants {@link * org.eclipse.microprofile.config.Config} implementations to be * acquired using {@link * org.eclipse.microprofile.config.ConfigProvider#getConfig()} * invocations. This constructor exists primarily for * convenience.

* *

Thread Safety

* *

{@code sources} will be synchronized on and iterated * over by this constructor, which may have implications on * the type of {@link Collection} supplied.

* * @param sources a {@link Collection} of {@link ConfigSource}s; may * be {@code null}; will be synchronized on and iterated * over; copied by value; no reference is retained to this * object * * @param typeConverter a {@link TypeConverter} implementation used * for type conversion; must not be {@code null}; if it implements * {@link Closeable} it will be {@linkplain * Closeable#close() closed} by this {@link Config}'s {@link * #close()} method * * @exception NullPointerException of {@code typeConverter} is * {@code null} * * @see ConfigProviderResolver * * @see org.eclipse.microprofile.config.ConfigProvider#getConfig() */ public Config(final Collection sources, final TypeConverter typeConverter) { super(); this.typeConverter = Objects.requireNonNull(typeConverter); if (sources == null) { this.sources = Collections.emptySet(); } else { synchronized (sources) { if (sources.isEmpty()) { this.sources = Collections.emptySet(); } else { this.sources = Collections.unmodifiableCollection(sources); } } } } /** * Closes this {@link Config} using a best-effort strategy. * *

An attempt is made to close each {@link ConfigSource} that is * itself an instance of {@link Closeable} and that was supplied to * this {@link Config} at construction time. If any errors occur * along the way, they are added as {@linkplain * Throwable#addSuppressed(Throwable) suppressed exceptions} to an * {@link IOException} which is thrown as a kind of * aggregate.

* *

When this method finishes normally, all of this {@link * Config}'s associated {@link Closeable} {@link ConfigSource}s will * be {@linkplain Closeable#close() closed}. In addition, * the {@link TypeConverter} supplied at construction time will be * closed as well if it implements {@link Closeable}.

* *

Thread Safety

* *

This method is idempotent and safe for concurrent use by * multiple threads.

* * @exception IOException if at least one {@link Closeable} {@link * ConfigSource} was not successfully closed or if the {@link * TypeConverter} supplied at construction time implements {@link * Closeable} and threw an {@link IOException} */ @Override public final void close() throws IOException { final boolean oldClosed = this.isClosed(); if (!oldClosed) { IOException throwMe = null; synchronized (this.sources) { for (final ConfigSource configSource : this.sources) { if (configSource instanceof Closeable) { try { ((Closeable)configSource).close(); } catch (final IOException ioException) { if (throwMe == null) { throwMe = ioException; } else { throwMe.addSuppressed(ioException); } } } } } if (this.typeConverter instanceof Closeable) { try { ((Closeable)this.typeConverter).close(); } catch (final IOException ioException) { if (throwMe == null) { throwMe = ioException; } else { throwMe.addSuppressed(ioException); } } } if (throwMe != null) { throw throwMe; } this.closed = true; ConfigProviderResolver.instance().releaseConfig(this); // XXX re-entrant call, potentially } } /** * Returns {@code true} if this {@link Config} has been {@linkplain * #close() closed}. * *

A closed {@link Config} will throw {@link * IllegalStateException} from most of its methods.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @return {@code true} if this {@link Config} has been {@linkplain * #close() closed}; {@code false} otherwise * * @see #close() */ public final boolean isClosed() { return this.closed; } /** * Returns an {@link Iterable} representing a snapshot at a moment * in time of this {@link Config}'s affiliated {@link ConfigSource}s * as they existed at that time. * *

This method never returns {@code null}.

* *

The underlying collection of {@link ConfigSource}s the * returned {@link Iterable} is capable of iterating over is an * immutable copy of the internal collection of {@link * ConfigSource}s managed by this {@link Config}.

* *

The {@link Iterable} returned by this method {@linkplain * Iterable#iterator() creates Iterators} that do not * support the {@link Iterator#remove()} method.

* *

The MicroProfile Config specification implies a state of * affairs that permits {@link ConfigSource}s "inside" a {@link * Config} to come and go. Consequently, this method returns what * is effectively a dissociated snapshot at a moment in time of a * collection of {@link ConfigSource}s that were once managed by * this {@link Config} at that moment in time.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* *

The {@link Iterable} returned by this method is safe for use * by multiple threads.

* * @return an {@link Iterable} of {@link ConfigSource}s; never * {@code null} * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} */ @Override public final Iterable getConfigSources() { if (this.isClosed()) { throw new IllegalStateException("this.isClosed()"); } final Iterable returnValue; synchronized (this.sources) { if (this.sources.isEmpty()) { returnValue = Collections.emptySet(); } else { returnValue = Collections.unmodifiableCollection(new ArrayList<>(this.sources)); } } return returnValue; } /** * Returns an {@link Iterable} representing a snapshot at a moment * in time of the configuration property names as they existed at * that time. * *

This method never returns {@code null}.

* *

The underlying collection of property names the returned * {@link Iterable} is capable of iterating over is an immutable * copy of the internal collection of configuration property names * managed by this {@link Config}.

* *

The {@link Iterable} returned by this method {@linkplain * Iterable#iterator() creates Iterators} that do not * support the {@link Iterator#remove()} method.

* *

The MicroProfile Config specification implies a state of * affairs that permits {@link ConfigSource}s "inside" a {@link * Config} to come and go. Consequently, this method returns what * is effectively a dissociated snapshot at a moment in time of a * collection of the configuration property names that were once * managed by this {@link Config} at that moment in time.

* *

No caching is performed by this method. Property names are * harvested from calls to {@link ConfigSource#getPropertyNames()} * on each {@link ConfigSource} present in the {@link Iterable} * returned by the {@link #getConfigSources()} method.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* *

The {@link Iterable} returned by this method is safe for use * by multiple threads.

* * @return an {@link Iterable} representing a snapshot of a * collection of configuration property names which this {@link * Config} manages; never {@code null} * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} */ @Override public final Iterable getPropertyNames() { final Iterable returnValue; final Iterable configSources = this.getConfigSources(); if (configSources == null) { returnValue = Collections.emptySet(); } else { final Set names = new TreeSet<>(); for (final ConfigSource configSource : configSources) { if (configSource != null) { // The specification says nothing about concurrency. synchronized (configSource) { final Collection sourceNames = configSource.getPropertyNames(); if (sourceNames != null && !sourceNames.isEmpty()) { names.addAll(sourceNames); } } } } returnValue = Collections.unmodifiableSet(names); } assert returnValue != null; return returnValue; } /** * Returns an {@link Optional} representing an optional * configuration property value for the supplied {@code name}, as * converted to an object of the supplied {@code type}. * *

This method never returns {@code null}.

* *

This method does not cache the value it returns.

* *

It is worth noting that the MicroProfile Config * specification does not say anything about whether {@link * Converter}s are allowed to attempt to "convert" {@code null} * values from {@link ConfigSource#getValue(String)} invocations * into non-{@code null} objects. The MicroProfile Config TCK will fail if {@link * Converter}s _do_ attempt to work on {@code null} values, so this * implementation never uses a {@code null} value for * conversion.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @param the type of the value being requested * * @param name the name of the configuration property whose * converted value should be returned; may be {@code null}; passed * unaltered to {@link ConfigSource#getValue(String)} so subject to * that method's (unspecified) preconditions * * @param type the {@link Class} representing the type any * non-{@code null} value should be converted to if possible; must * not be {@code null} * * @return an {@link Optional} representing an optional * configuration property value for the supplied {@code name}, as * converted to an object of the supplied {@code type}; never {@code * null} * * @exception IllegalArgumentException if the value could not be * converted to the requested type * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} * * @see #getOptionalValue(String, Type) * * @see #getValue(String, Class) * * @see #getValue(String, Type) * * @see ConfigSource#getValue(String) * * @see TypeConverter#convert(String, Type) * * @see Converter#convert(String) */ @Override public final Optional getOptionalValue(final String name, final Class type) { return this.getOptionalValue(name, (Type)type); } /** * Returns an {@link Optional} representing an optional * configuration property value for the supplied {@code name}, as * converted to an object of the supplied {@code type}. * *

This method never returns {@code null}.

* *

This method does not cache the value it returns.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @param the type of the value being requested * * @param name the name of the configuration property whose * converted value should be returned; may be {@code null}; passed * unaltered to {@link ConfigSource#getValue(String)} so subject to * that method's (unspecified) preconditions * * @param type the {@link Type} representing the type any * non-{@code null} value should be converted to if possible; must * not be {@code null} * * @return an {@link Optional} representing an optional * configuration property value for the supplied {@code name}, as * converted to an object of the supplied {@code type}; never {@code * null} * * @exception IllegalArgumentException if the value could not be * converted to the requested type * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} * * @see #getOptionalValue(String, Class) * * @see #getValue(String, Class) * * @see #getValue(String, Type) * * @see ConfigSource#getValue(String) * * @see TypeConverter#convert(String, Type) * * @see Converter#convert(String) */ public final Optional getOptionalValue(final String name, final Type type) { Objects.requireNonNull(type); Optional returnValue = null; final Iterable configSources = this.getConfigSources(); if (configSources != null) { for (final ConfigSource configSource : configSources) { if (configSource != null) { final String value = configSource.getValue(name); if (value != null) { returnValue = Optional.of(this.convert(value, type)); assert returnValue != null; if (returnValue.isPresent()) { break; } } } } } if (returnValue == null) { returnValue = Optional.empty(); } return returnValue; } /** * Returns the value for the configuration property named by the * supplied {@code name}, as converted to an object of the supplied * {@code type}. * *

This method never returns {@code null}.

* *

This method does not cache the value it returns.

* *

The MicroProfile Config specification does not say whether * implementations of the {@link * org.eclipse.microprofile.config.Config#getValue(String, Class)} * method may or may not return {@code null}. {@code null} values * from {@link ConfigSource#getValue(String)} invocations are taken * to indicate a given configuration property value's absence, * however. Coupled with the fact that all {@link * org.eclipse.microprofile.config.Config#getValue(String, Class)} * are required to throw {@link NoSuchElementException}s when "the * property isn't present in the configuration", this implementation * chooses never to return {@code null}.

* *

It is also worth noting that the MicroProfile Config * specification does not say anything about whether {@link * Converter}s are allowed to attempt to "convert" {@code null} * values from {@link ConfigSource#getValue(String)} invocations * into non-{@code null} objects. The MicroProfile Config TCK will fail if {@link * Converter}s _do_ attempt to work on {@code null} values, so this * implementation never uses a {@code null} value for * conversion.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @param the type of the values returned by this method * * @param name the name of the configuration property whose value * should be returned; may be {@code null}; passed unaltered to * {@link ConfigSource#getValue(String)} so subject to that method's * (unspecified) preconditions * * @param type the {@link Class} representing the type any * non-{@code null} value should be converted to if possible; must * not be {@code null} * * @return the converted value; never {@code null} * * @exception NoSuchElementException if the requested configuration * property value does not exist * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} * * @see ConfigSource#getValue(String) * * @see #getValue(String, Type) * * @see #getOptionalValue(String, Class) * * @see #getOptionalValue(String, Type) */ @Override public final T getValue(final String name, final Class type) { return this.getValue(name, (Type)type); } /** * Returns the value for the configuration property named by the * supplied {@code name}, as converted to an object of the supplied * {@code type}. * *

This method never returns {@code null}.

* *

This method does not cache the value it returns.

* *

It is also worth noting that the MicroProfile Config * specification does not say anything about whether {@link * Converter}s are allowed to attempt to "convert" {@code null} * values from {@link ConfigSource#getValue(String)} invocations * into non-{@code null} objects. The MicroProfile Config TCK will fail if {@link * Converter}s _do_ attempt to work on {@code null} values, so this * implementation never uses a {@code null} value for * conversion.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @param the type of the values returned by this method * * @param name the name of the configuration property whose value * should be returned; may be {@code null}; passed unaltered to * {@link ConfigSource#getValue(String)} so subject to that method's * (unspecified) preconditions * * @param type the {@link Type} representing the type any * non-{@code null} value should be converted to if possible; must * not be {@code null} * * @return the converted value; never {@code null} * * @exception NoSuchElementException if the requested configuration * property value does not exist * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} * * @see ConfigSource#getValue(String) * * @see #getValue(String, Class) * * @see #getOptionalValue(String, Class) * * @see #getOptionalValue(String, Type) */ public final T getValue(final String name, final Type type) { Objects.requireNonNull(type); T returnValue = null; final Iterable configSources = this.getConfigSources(); if (configSources != null) { for (final ConfigSource configSource : configSources) { if (configSource != null) { final String value = configSource.getValue(name); if (value != null) { returnValue = this.convert(value, type); if (returnValue != null) { break; } } } } } if (returnValue == null) { throw new NoSuchElementException(name); } return returnValue; } /** * Implements the {@link TypeConverter#convert(String, Type)} method * by invoking the same method on this {@link Config}'s affiliated * {@link TypeConverter} supplied at construction time and returning * the result. * *

This method may return {@code null}.

* *

Thread Safety

* *

This method is safe for use by multiple threads.

* * @param rawValue the value to convert; may be {@code null} * * @param type the {@link Type} to which the current invocation of * this method's return value should be assignable; must not be * {@code null} * * @return an object assignable to the supplied {@code type}, or * {@code null} * * @see TypeConverter#convert(String, Type) * * @exception NullPointerException if {@code type} is {@code null} * * @exception IllegalArgumentException if conversion could not occur * for any reason * * @exception IllegalStateException if this {@link Config} has been * {@linkplain #close() closed} */ @Override public final T convert(final String rawValue, final Type type) { if (this.isClosed()) { throw new IllegalStateException("this.isClosed()"); } return this.typeConverter.convert(rawValue, type); } static final Collection getDefaultConfigSources() throws IOException { return getDefaultConfigSources(null); } static final Collection getDefaultConfigSources(final ClassLoader classLoader) throws IOException { final Collection sources = new LinkedList<>(); sources.add(new SystemPropertiesConfigSource()); sources.add(new EnvironmentVariablesConfigSource()); final Collection microprofileConfigPropertiesConfigSources = getMicroprofileConfigPropertiesSources(classLoader); if (microprofileConfigPropertiesConfigSources != null && !microprofileConfigPropertiesConfigSources.isEmpty()) { sources.addAll(microprofileConfigPropertiesConfigSources); } return Collections.unmodifiableCollection(sources); } static final Collection getMicroprofileConfigPropertiesSources(ClassLoader classLoader) throws IOException { if (classLoader == null) { classLoader = AccessController.doPrivileged((PrivilegedAction)() -> Thread.currentThread().getContextClassLoader()); } final Enumeration urls = classLoader.getResources("META-INF/microprofile-config.properties"); Collection returnValue = new ArrayList<>(); if (urls != null) { while (urls.hasMoreElements()) { final URL url = urls.nextElement(); if (url != null) { final Properties properties = new Properties(); // The specification does not mandate a character set for // the /META-INF/microprofile-config.properties, nor whether // it should be in java.util.Properties format. We'll // assume ISO-8859-1 and java.util.Properties format. try (final Reader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.ISO_8859_1))) { properties.load(reader); } // The specification provides no guidance on ConfigSource naming. returnValue.add(new PropertiesConfigSource(properties, url.toString())); } } } if (returnValue.isEmpty()) { returnValue = Collections.emptySet(); } else { returnValue = Collections.unmodifiableCollection(returnValue); } return returnValue; } static final Collection getDiscoveredConfigSources(ClassLoader classLoader) { if (classLoader == null) { classLoader = AccessController.doPrivileged((PrivilegedAction)() -> Thread.currentThread().getContextClassLoader()); } final ServiceLoader discoveredSources = ServiceLoader.load(ConfigSource.class, classLoader); assert discoveredSources != null; final Collection sources = new LinkedList<>(); for (final ConfigSource source : discoveredSources) { if (source != null) { sources.add(source); } } final ServiceLoader discoveredConfigSourceProviders = ServiceLoader.load(ConfigSourceProvider.class, classLoader); assert discoveredConfigSourceProviders != null; for (final ConfigSourceProvider provider : discoveredConfigSourceProviders) { if (provider != null) { final Iterable configSources = provider.getConfigSources(classLoader); if (configSources != null) { for (final ConfigSource configSource : configSources) { if (configSource != null) { sources.add(configSource); } } } } } return Collections.unmodifiableCollection(sources); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy