![JAR search and dependency download from the Maven repository](/logo.png)
org.microbean.microprofile.config.ConversionHub 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.beans.PropertyEditor;
import java.beans.PropertyEditorManager;
import java.io.Closeable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.regex.Pattern;
import org.eclipse.microprofile.config.spi.Converter;
/**
* A {@link Serializable}, {@link Closeable} {@link TypeConverter}
* implementation that is based on a collection of {@link Converter}s.
*
* @author Laird Nelson
*
* @see #convert(String, Type)
*/
public class ConversionHub implements Closeable, Serializable, TypeConverter {
private static final long serialVersionUID = 1L;
private static final Pattern splitPattern = Pattern.compile("(?, Class>> wrapperClasses;
static {
wrapperClasses = new HashMap<>();
wrapperClasses.put(boolean.class, Boolean.class);
wrapperClasses.put(byte.class, Byte.class);
wrapperClasses.put(char.class, Character.class);
wrapperClasses.put(double.class, Double.class);
wrapperClasses.put(float.class, Float.class);
wrapperClasses.put(int.class, Integer.class);
wrapperClasses.put(long.class, Long.class);
wrapperClasses.put(short.class, Short.class);
}
private final Map> converters;
private volatile boolean closed;
/**
* Creates a new {@link ConversionHub}.
*/
public ConversionHub() {
super();
final Map extends Type, ? extends Converter>> discoveredConverters = getDiscoveredConverters(null);
this.converters = new HashMap<>(discoveredConverters);
}
/**
* Creates a new {@link ConversionHub}.
*
* Thread Safety
*
* {@code converters} will be synchronized on and iterated
* over by this constructor, which may have implications on
* the type of {@link Map} supplied.
*
* @param converters a {@link Map} of {@link Converter} instances,
* indexed by the {@link Type} describing the type of the return
* value of their respective {@link Converter#convert(String)}
* methods; may be {@code null}; will be synchronized on and
* iterated over; copied by value; no reference is kept to
* this object
*/
public ConversionHub(final Map extends Type, ? extends Converter>> converters) {
super();
if (converters == null) {
this.converters = new HashMap<>();
} else {
synchronized (converters) {
this.converters = new HashMap<>(converters);
}
}
}
/**
* Closes this {@link ConversionHub} using a best-effort strategy.
*
* This method attempts to close each of this {@link
* ConversionHub}'s associated {@link Closeable} {@link Converter}s.
* Any {@link IOException} thrown during such an attempt does not
* abort the closing process.
*
* Once this method has been invoked:
*
*
*
* - All future invocations of the {@link #convert(String, Type)}
* method will throw an {@link IllegalStateException}
*
* - All future invocations of the {@link #isClosed()} method will
* return {@code true}
*
*
*
* {@link ConversionHub} instances are often {@linkplain
* Config#Config(Collection, TypeConverter) supplied to
* Config
instances at construction time}, and so may
* be {@linkplain Config#close() closed by them}.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @exception IOException if at least one underlying {@link
* Closeable} {@link Converter} could not be closed
*/
@Override
public void close() throws IOException {
if (!this.isClosed()) {
/*
The specification says:
"A factory method ConfigProviderResolver#releaseConfig(Config
config) to release the Config instance [sic]. This will unbind
the current Config from the application. The ConfigSources
that implement the java.io.Closeable interface will be
properly destroyed. The Converters that implement the
java.io.Closeable interface will be properly destroyed."
It is not clear which ConfigSources and which Converters are
meant here, but assuming they are only those ones present "in"
the Config being released, there's no way to "get" those from
a given Config, since (a) there is no requirement that a
Config actually house Converters and (b) consequently there is
nothing like a Config#getConverters() method.
So we implement Closeable to at least provide the ability to
close everything cleanly and in a thread-safe manner.
*/
IOException throwMe = null;
synchronized (this.converters) {
if (!this.converters.isEmpty()) {
final Collection extends Converter>> converters = this.converters.values();
assert converters != null;
assert !converters.isEmpty();
for (final Converter> converter : converters) {
if (converter instanceof Closeable) {
try {
((Closeable)converter).close();
} catch (final IOException ioException) {
if (throwMe == null) {
throwMe = ioException;
} else {
throwMe.addSuppressed(ioException);
}
}
}
}
}
}
if (throwMe != null) {
throw throwMe;
}
this.closed = true;
}
}
/**
* Returns {@code true} if this {@link ConversionHub} has been
* {@linkplain #close() closed}.
*
* All invocations of the {@link #convert(String, Type)} method
* will always throw an {@link IllegalStateException} once this
* {@link ConversionHub} has been {@linkplain #close() closed}.
*
* Thread Safety
*
* This method is idempotent and safe for concurrent use by
* multiple threads.
*
* @return {@code true} if this {@link ConversionHub} has been
* {@linkplain #close() closed}; {@code false} otherwise
*
* @see #close()
*/
public final boolean isClosed() {
return this.closed;
}
/**
* Attempts to convert the supplied {@link String} value to an
* object assignable to the supplied {@link Type}, throwing an
* {@link IllegalArgumentException} if such conversion is
* impossible.
*
* This method may return {@code null}.
*
* Thread Safety
*
* This method is safe for concurrent use by multiple
* threads.
*
* @param value the value to convert; may be {@code null}
*
* @param type the {@link Type} to which the value should be
* converted; must not be {@code null}; the type of the return value
* resulting from invocations this method should be assignable to
* references of this type
*
* @return the converted object, which may be {@code null}
*
* @exception IllegalArgumentException if conversion could not occur
* for any reason
*
* @exception IllegalStateException if this {@link ConversionHub}
* was {@linkplain #close() closed}
*/
@Override
@SuppressWarnings("unchecked")
public final T convert(final String value, final Type type) {
if (this.isClosed()) {
throw new IllegalStateException("this.isClosed()");
}
Converter converter;
synchronized (this.converters) {
converter = (Converter)this.converters.get(type);
if (converter == null) {
try {
converter = this.computeConverter(type);
} catch (final ReflectiveOperationException reflectiveOperationException) {
throw new IllegalArgumentException(reflectiveOperationException.getMessage(), reflectiveOperationException);
}
if (converter != null) {
this.converters.put(type, converter);
}
}
}
if (converter == null) {
throw new IllegalArgumentException("\"" + value + "\" could not be converted to " + (type == null ? "null" : type.getTypeName()));
}
final T returnValue = converter.convert(value);
return returnValue;
}
@SuppressWarnings("unchecked")
private final Converter computeConverter(final Type conversionType) throws ReflectiveOperationException {
Converter returnValue = null;
if (CharSequence.class.equals(conversionType) ||
String.class.equals(conversionType) ||
Serializable.class.equals(conversionType) ||
Object.class.equals(conversionType)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
return (T)rawValue;
}
};
} else if (Boolean.class.equals(conversionType) || boolean.class.equals(conversionType)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
return (T)Boolean.valueOf(rawValue != null &&
("true".equalsIgnoreCase(rawValue) ||
"y".equalsIgnoreCase(rawValue) ||
"yes".equalsIgnoreCase(rawValue) ||
"on".equalsIgnoreCase(rawValue) ||
"1".equals(rawValue)));
}
};
} else if (Character.class.equals(conversionType) || char.class.equals(conversionType)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
if (rawValue == null || rawValue.isEmpty()) {
return null;
} else if (rawValue.length() != 1) {
throw new IllegalArgumentException("Unexpected length for character conversion: " + rawValue);
}
return (T)Character.valueOf(rawValue.charAt(0));
}
};
} else if (URL.class.equals(conversionType)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
try {
return (T)URI.create(rawValue).toURL();
} catch (final MalformedURLException malformedUrlException) {
throw new IllegalArgumentException(malformedUrlException.getMessage(), malformedUrlException);
}
}
};
} else if (Class.class.equals(conversionType)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
try {
// Seems odd that the specification mandates the use of
// the single-argument Class#forName(String) method, but
// it's spelled out in black and white:
// https://github.com/eclipse/microprofile-config/blob/20e1d59dd1055867a54e65b77405f9e68611544e/spec/src/main/asciidoc/converters.asciidoc#built-in-converters
// See
// https://github.com/eclipse/microprofile-config/issues/424.
return (T)Class.forName(rawValue);
} catch (final ClassNotFoundException classNotFoundException) {
throw new IllegalArgumentException(classNotFoundException.getMessage(), classNotFoundException);
}
}
};
} else if (conversionType instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType)conversionType;
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
assert actualTypeArguments != null;
assert actualTypeArguments.length > 0;
final Type rawType = parameterizedType.getRawType();
assert rawType instanceof Class : "!(parameterizedType.getRawType() instanceof Class): " + rawType;
final Class> conversionClass = (Class>)rawType;
assert !conversionClass.isArray();
if (Optional.class.isAssignableFrom(conversionClass)) {
assert actualTypeArguments.length == 1;
final Type firstTypeArgument = actualTypeArguments[0];
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
return (T)Optional.ofNullable(ConversionHub.this.convert(rawValue, firstTypeArgument)); // XXX recursive call
}
};
} else if (Class.class.isAssignableFrom(conversionClass)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
return ConversionHub.this.convert(rawValue, conversionClass); // XXX recursive call
}
};
} else if (Collection.class.isAssignableFrom(conversionClass)) {
returnValue = new SerializableConverter() {
private static final long serialVersionUID = 1L;
@Override
public final T convert(final String rawValue) {
Collection