org.microbean.microprofile.config.MicroProfileConfigProperties Maven / Gradle / Ivy
/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
*
* Copyright © 2019 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.security.AccessController;
import java.security.PrivilegedAction;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
/**
* A {@link Properties} implementation that wraps a {@link Config} and
* is suitable for {@linkplain System#setProperties(Properties)
* installing as System
properties}.
*
* This class intentionally violates several tenets of the
* {@link Map} interface and {@link Properties} class.
* Deviations and oddities are noted below.
*
*
*
* - The {@link #clear()} method, while supported, cannot, and
* therefore does not, remove any items from the {@link Config} that
* is expressed by this class. It will remove all items manually
* {@linkplain #put(Object, Object) put} into a {@link
* MicroProfileConfigProperties} instance directly.
*
* - Because the MicroProfile Config specification does not say
* anything about concurrent access to methods like {@link
* Config#getPropertyNames()}, all iteration methods in this class
* should be regarded as producing unmodifiable snapshots of a rough
* estimate of the keys present in a {@link
* MicroProfileConfigProperties} instance.
*
* - All iteration return values are immutable and will throw {@link UnsupportedOperationException} where appropriate.
*
* - Because the MicroProfile Config specification does not say
* anything about the underlying dynamic nature of the configuration
* systems that a given {@link Config} abstracts, it is possible, for
* example, for the {@link #containsKey(Object)} method to return
* {@code true}, and then for {@link #get(Object)} to be unable to
* retrieve a value.
*
* - Because the MicroProfile Config specification does not say
* anything about what implementations must do with regards to caching
* or freshness, it is strictly speaking undefined whether fresh or
* stale results will be retrieved by instances of this class.
*
*
*
* Thread Safety
*
* This class is safe for concurrent use by multiple threads.
*
* @author Laird Nelson
*
* @see Config
*/
public class MicroProfileConfigProperties extends Properties {
/*
* Static fields.
*/
/**
* The version of this class for {@linkplain Serializable
* serialization} purposes.
*/
private static final long serialVersionUID = 1L;
/*
* Instance fields.
*/
/**
* The {@link Config} providing configuration property values.
*
* This field is never {@code null}.
*/
private final Config config;
/**
* A flag indicating whether an iteration is in progress; useful to
* avoid infinite loops when this {@link
* MicroProfileConfigProperties} is {@linkplain
* System#setProperties(Properties) installed as System
* properties}
*/
// @GuardedBy("this")
private boolean iterating;
/*
* Constructors.
*/
/**
* Creates a new {@link MicroProfileConfigProperties} representing
* the {@link Config} that results from {@link
* ConfigProvider#getConfig()}.
*
* @see ConfigProvider#getConfig()
*/
public MicroProfileConfigProperties() {
this(null, null);
}
/**
* Creates a new {@link MicroProfileConfigProperties} wrapping the
* supplied {@link Config}.
*
* @param config the {@link Config} to express; may be {@code null}
* in which case the return value of {@link
* ConfigProvider#getConfig()} will be used instead
*
* @see ConfigProvider#getConfig()
*/
public MicroProfileConfigProperties(final Config config) {
this(config, null);
}
/**
* Creates a new {@link MicroProfileConfigProperties} using the
* supplied {@link Properties} as its {@linkplain
* Properties#defaults defaults} and wrapping the {@link Config}
* that results from {@link ConfigProvider#getConfig()}.
*
* @param defaults the default {@link Properties}; may be {@code null}
*
* @see Properties#defaults
*
* @see ConfigProvider#getConfig()
*/
public MicroProfileConfigProperties(final Properties defaults) {
this(null, defaults);
}
/**
* Creates a new {@link MicroProfileConfigProperties} using the
* supplied {@link Properties} as its {@linkplain
* Properties#defaults defaults} and wrapping the supplied {@link
* Config}.
*
* @param config the {@link Config} to express; may be {@code null}
* in which case the return value of {@link
* ConfigProvider#getConfig()} will be used instead
*
* @param defaults the default {@link Properties}; may be {@code
* null}
*
* @see ConfigProvider#getConfig()
*/
public MicroProfileConfigProperties(final Config config, final Properties defaults) {
super(defaults);
this.config = config == null ? ConfigProvider.getConfig() : config;
}
/*
* Instance methods.
*/
/**
* Returns {@code true} if this {@link MicroProfileConfigProperties}
* {@linkplain Properties#containsKey(Object) directly contains} the
* supplied {@code key}, or if the supplied {@code key} is a {@link
* String} and is contained in the {@link Set} of configuration
* property names returned by the {@link Config#getPropertyNames()}
* method.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param key the key to seek; may be {@code null}
*
* @return {@code true} if this {@link MicroProfileConfigProperties}
* {@linkplain Properties#containsKey(Object) directly contains} the
* supplied {@code key}, or if the supplied {@code key} is a {@link
* String} and is contained in the {@link Set} of configuration
* property names returned by the {@link Config#getPropertyNames()}
* method; {@code false} otherwise
*/
@Override
public synchronized final boolean containsKey(final Object key) {
boolean returnValue = super.containsKey(key);
if (!returnValue) {
final Iterable> propertyNames = this.config.getPropertyNames();
if (propertyNames != null) {
if (key == null) {
for (final Object propertyName : propertyNames) {
if (propertyName == null) {
returnValue = true;
break;
}
}
} else {
for (final Object propertyName : propertyNames) {
if (key.equals(propertyName)) {
returnValue = true;
break;
}
}
}
}
}
return returnValue;
}
/**
* Invokes the {@link #containsValue(Object)} method with the
* supplied {@link Object} and returns the result.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param value the value to seek; may be {@code null}
*
* @return {@code true} if the {@link #containsValue(Object)} method
* returns {@code true}; {@code false} otherwise
*
* @see #containsValue(Object)
*/
@Override
public synchronized final boolean contains(final Object value) {
return this.containsValue(value);
}
/**
* Returns {@code true} if this {@link MicroProfileConfigProperties}
* contains the supplied value {@link Object}.
*
* First the {@link Properties#containsValue(Object)} method is
* invoked with the supplied {@link Object}. If that returns {@code
* true}, then {@code true} is returned.
*
* Next, {@linkplain Config#getPropertyNames() all known property
* names in the Config
wrapped by this
* MicroProfileConfigProperties
} are acquired. This
* set is iterated over and {@link Config#getOptionalValue(String,
* Class)} is called for each one. If the resulting {@link
* Optional} {@linkplain Optional#isPresent() is present}, then
* {@code true} is returned.
*
* In all other cases {@code false} is returned.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param value the value to seek; may be {@code null}
*
* @return {@code true} if this {@link MicroProfileConfigProperties}
* contains the supplied value; {@code false} otherwise
*
* @see Config#getPropertyNames()
*
* @see Properties#containsValue(Object)
*/
@Override
public synchronized final boolean containsValue(final Object value) {
boolean returnValue = super.containsValue(value);
if (!returnValue) {
final Iterable extends String> propertyNames = this.config.getPropertyNames();
if (propertyNames == null) {
returnValue = false;
} else {
for (final String propertyName : propertyNames) {
final Optional> propertyValue = this.config.getOptionalValue(propertyName, String.class);
if (propertyValue != null && propertyValue.isPresent()) {
returnValue = true;
break;
}
}
}
}
return returnValue;
}
/**
* Returns the {@link String} value indexed under the supplied
* {@code key}.
*
* This method may return {@code null}.
*
* This implementation first calls {@link #get(Object)}. If the
* result is a non-{@code null} {@link String}, then it is returned.
* Otherwise, if the {@link #defaults} field is non-{@code null},
* then {@link Properties#getProperty(String)} is invoked on it with
* the supplied {@code key} and the result is returned. In all
* other cases {@code null} is returned.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param key the key of the value to return; may be {@code null}
*
* @return an appropriate value, or {@code null}
*
* @see #get(Object)
*
* @see #defaults
*
* @see Properties#getProperty(String)
*/
@Override
public synchronized final String getProperty(final String key) {
final String returnValue;
final Object propertyValue = this.get(key);
if (propertyValue instanceof String) {
returnValue = (String)propertyValue;
} else if (this.defaults != null) {
returnValue = this.defaults.getProperty(key);
} else {
returnValue = null;
}
return returnValue;
}
/**
* Returns the value indexed under the supplied {@code key}, or
* {@code null} if the value does not exist. Note that a {@code
* null} return value could result from a key's being explicitly
* mapped to {@code null}, or from a key's absence.
*
* This implementation first calls {@link
* Properties#containsKey(Object)} with the supplied {@code key}. If
* that method invocation returns {@code true}, then the {@link
* Properties#get(Object)} method is invoked and its result is
* returned.
*
* Otherwise, the {@link Config#getOptionalValue(String, Class)}
* method is called and its resulting {@link Optional} result
* {@linkplain Optional#get() is acquired} and returned, or, if it
* is {@linkplain Optional#isPresent() not present}, {@code null} is
* returned.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param key the key of the value to return; may be {@code null}
*
* @return an appropriate value, or {@code null}
*
* @see Properties#containsKey(Object)
*
* @see Properties#get(Object)
*
* @see Config#getOptionalValue(String, Class)
*/
@Override
public synchronized final Object get(final Object key) {
final Object returnValue;
if (super.containsKey(key) || this.iterating) {
returnValue = super.get(key);
} else {
this.iterating = true;
try {
final Optional configValue = this.config.getOptionalValue(key.toString(), String.class);
if (configValue == null || !configValue.isPresent()) {
returnValue = null;
} else {
returnValue = configValue.get();
}
} finally {
iterating = false;
}
}
return returnValue;
}
/**
* Returns {@code true} if this {@link MicroProfileConfigProperties}
* is empty. In all normal cases, this method will return {@code
* false}, since normally {@link Config} instances expose at least
* one configuration property value.
*
* This implementation calls the {@link Properties#isEmpty()}
* method. If that method returns {@code false}, then {@code false}
* is returned.
*
* Otherwise this method calls the {@link
* Config#getPropertyNames()} method, calls {@link
* Iterable#iterator()} on the resulting {@link Iterable}, and, if
* it is non-{@code null}, calls the {@link Iterator#hasNext()}
* method on it, returning its result.
*
* In all other cases this method returns {@code true}.
*
* This method is a much faster way of checking if this {@link
* MicroProfileConfigProperties}' size is {@code 0}.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @return {@code true}, rarely, if this {@link
* MicroProfileConfigProperties} is empty; {@code false} otherwise
*
* @see Properties#isEmpty()
*
* @see Config#getPropertyNames()
*/
@Override
public synchronized final boolean isEmpty() {
boolean returnValue = super.isEmpty();
if (returnValue) {
final Iterable> propertyNames = this.config.getPropertyNames();
if (propertyNames != null) {
final Iterator> iterator = propertyNames.iterator();
returnValue = iterator != null && iterator.hasNext();
}
}
return returnValue;
}
/**
* Returns the size of this {@link MicroProfileConfigProperties} as
* expressed by the size of its {@linkplain #keySet() key set}.
*
* This method returns {@code int}s that are greater than or equal to zero.
*
* This method rarely returns {@code 0} given the fact that a
* {@link Config} normally expresses at least one configuration
* property value.
*
* Use the {@link #isEmpty()} method for a much, much faster way
* to check for a size of {@code 0}.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @return the size of this {@link MicroProfileConfigProperties}
*
* @see #keySet()
*
* @see #isEmpty()
*/
@Override
public synchronized final int size() {
return this.keySet().size();
}
/**
* Invokes the {@link #keySet()} method and returns its return
* value.
*
* This method never returns {@code null}.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @return the result of invoking the {@link #keySet()} method.
*
* @see #keySet()
*/
@Override
public synchronized final Enumeration