com.couchbase.client.core.env.BuilderPropertySetter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-io Show documentation
Show all versions of core-io Show documentation
The official Couchbase JVM Core IO Library
/*
* Copyright 2019 Couchbase, Inc.
*
* 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 com.couchbase.client.core.env;
import com.couchbase.client.core.deps.com.fasterxml.jackson.core.type.TypeReference;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.util.Golang;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
@SuppressWarnings("rawtypes")
class BuilderPropertySetter {
/**
* @throws InvalidPropertyException if the property could not be applied to the builder
*/
public void set(Object builder, String propertyName, String propertyValue) {
// By convention, builder methods that return child builders have names ending with this.
final String CHILD_BUILDER_ACCESSOR_SUFFIX = "Config";
try {
final List propertyComponents = Arrays.asList(propertyName.split("\\.", -1));
final List pathToBuilder = propertyComponents.subList(0, propertyComponents.size() - 1);
final String setterName = propertyComponents.get(propertyComponents.size() - 1);
for (String pathComponent : pathToBuilder) {
try {
final String childBuilderAccessor = pathComponent + CHILD_BUILDER_ACCESSOR_SUFFIX;
builder = builder.getClass().getMethod(childBuilderAccessor).invoke(builder);
} catch (NoSuchMethodException e) {
throw InvalidArgumentException.fromMessage("Method not found: " + e.getMessage(), e);
}
}
final List candidates = Arrays.stream(builder.getClass().getMethods())
.filter(m -> m.getName().equals(setterName))
.filter(m -> m.getParameterCount() == 1)
.collect(Collectors.toList());
if (candidates.isEmpty()) {
throw InvalidArgumentException.fromMessage("No one-arg setter for property \"" + propertyName + "\" in " + builder.getClass());
}
int remainingCandidates = candidates.size();
final List failedCandidates = new ArrayList<>();
for (Method setter : candidates) {
try {
final Object convertedValue = typeRegistry.convert(propertyValue, setter.getGenericParameterTypes()[0]);
setter.invoke(builder, convertedValue);
} catch (Throwable t) {
if (candidates.size() == 1) {
throw t;
}
failedCandidates.add(t);
if (--remainingCandidates == 0) {
final InvalidArgumentException e = InvalidArgumentException.fromMessage(
"Found multiple one-arg setters for property \"" + propertyName + "\" in "
+ builder.getClass() + " but none accepted the value \"" + propertyValue + "\".");
failedCandidates.forEach(e::addSuppressed);
throw e;
}
}
}
} catch (InvocationTargetException e) {
throw InvalidPropertyException.forProperty(propertyName, propertyValue, e.getCause());
} catch (Exception e) {
throw InvalidPropertyException.forProperty(propertyName, propertyValue, e);
}
}
public static class TypeConverterRegistry {
private final Map converters = new HashMap<>();
public TypeConverterRegistry register(Type t, TypeConverter converter) {
converters.put(requireNonNull(t), requireNonNull(converter));
return this;
}
public TypeConverterRegistry register(Type t, String expectation, Function conversion) {
requireNonNull(expectation);
requireNonNull(conversion);
return register(t, new TypeConverter() {
@Override
public String expectation(Type t, TypeConverterRegistry registry) {
return expectation;
}
@Override
public Object convert(String s, TypeConverterRegistry registry, Type t) {
return conversion.apply(s);
}
});
}
public Object convert(String value, Type targetType) {
final TypeConverter converter = converters.get(getRawType(targetType));
if (converter != null) {
try {
return converter.convert(value, this, targetType);
} catch (Exception e) {
throw InvalidArgumentException.fromMessage(
"Expected " + converter.expectation(targetType, this) + " but got \"" + value + "\".", e);
}
}
final Optional arrayComponentType = getArrayComponentType(targetType);
if (arrayComponentType.isPresent()) {
return convertArray(arrayComponentType.get(), value);
}
// Maybe it's an enum. We know how to convert those.
Optional enumClass = asEnumClass(targetType);
if (enumClass.isPresent()) {
return convertEnum(enumClass.get(), value);
}
// Maybe it has a `valueOf` factory method.
if (targetType instanceof Class) {
try {
final Method factoryMethod = ((Class) targetType).getMethod("valueOf", String.class);
final int modifiers = factoryMethod.getModifiers();
if (Modifier.isStatic(modifiers)
&& Modifier.isPublic(modifiers)
&& Arrays.equals(factoryMethod.getParameterTypes(), new Class[]{String.class})) {
return factoryMethod.invoke(null, value);
}
} catch (NoSuchMethodException e) {
// oh well.
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
// Maybe Jackson can convert it for us.
try {
return Mapper.reader().forType(asTypeReference(targetType)).readValue(value);
} catch (IOException e) {
throw InvalidArgumentException.fromMessage("Expected a value Jackson can bind to " + targetType + " but got \"" + value + "\".", e);
}
}
private Object convertArray(Class elementClass, String value) {
try {
List items = splitList(value);
Object array = Array.newInstance(elementClass, items.size());
int i = 0;
for (String item : items) {
Array.set(array, i++, convert(item, elementClass));
}
return array;
} catch (IllegalArgumentException e) {
throw InvalidArgumentException.fromMessage("Expected a comma-delimited list where each item is " + expectation(elementClass) + " but got \"" + value + "\".");
}
}
public String expectation(Type type) {
final TypeConverter converter = converters.get(type);
if (converter != null) {
return converter.expectation(type, this);
}
final Optional enumClass = asEnumClass(type);
if (enumClass.isPresent()) {
return "one of " + Arrays.asList(enumClass.get().getEnumConstants());
}
throw InvalidArgumentException.fromMessage("No converter for " + type);
}
}
@SuppressWarnings("unchecked")
private static Object convertEnum(Class enumClass, String value) {
try {
return Enum.valueOf(enumClass, value);
} catch (IllegalArgumentException e) {
throw InvalidArgumentException.fromMessage("Expected one of " +
Arrays.toString(enumClass.getEnumConstants()) + " but got \"" + value + "\"");
}
}
private static Class getRawType(Type t) {
return t instanceof Class ? (Class) t : (Class) ((ParameterizedType) t).getRawType();
}
interface TypeConverter {
String expectation(Type type, TypeConverterRegistry registry);
Object convert(String value, TypeConverterRegistry registry, Type type);
default TypeConverter simple(String expectation, Function conversion) {
requireNonNull(expectation);
requireNonNull(conversion);
return new TypeConverter() {
@Override
public String expectation(Type type, TypeConverterRegistry registry) {
return expectation;
}
@Override
public Object convert(String s, TypeConverterRegistry registry, Type t) {
return conversion.apply(s);
}
};
}
}
private final TypeConverterRegistry typeRegistry = new TypeConverterRegistry();
{
typeRegistry
.register(String.class, "a string", Function.identity())
.register(Integer.class, "an int", Integer::parseInt)
.register(Integer.TYPE, "an int", Integer::parseInt)
.register(Long.class, "a long", Long::parseLong)
.register(Long.TYPE, "a long", Long::parseLong)
.register(Double.class, "a double", Double::parseDouble)
.register(Double.TYPE, "a double", Double::parseDouble)
.register(Float.class, "a float", Float::parseFloat)
.register(Float.TYPE, "a float", Float::parseFloat)
.register(Boolean.class, "a boolean (\"true\", \"false\", \"1\", or \"0\")", BuilderPropertySetter::parseBooleanStrict)
.register(Boolean.TYPE, "a boolean (\"true\", \"false\", \"1\", or \"0\")", BuilderPropertySetter::parseBooleanStrict)
.register(Duration.class, "a duration qualified by a time unit (like \"2.5s\" or \"300ms\")",
d -> requireNonNegative(Golang.parseDuration(d)))
.register(Path.class, "an open file from a path", Paths::get)
.register(Iterable.class, new CollectionConverter(ArrayList.class))
.register(Collection.class, new CollectionConverter(ArrayList.class))
.register(List.class, new CollectionConverter(ArrayList.class))
.register(Set.class, new CollectionConverter(LinkedHashSet.class))
;
}
private static class CollectionConverter implements TypeConverter {
private final Class collectionClass;
public CollectionConverter(Class collectionClass) {
this.collectionClass = requireNonNull(collectionClass);
newCollection(); // fail fast
}
@Override
public String expectation(Type type, TypeConverterRegistry registry) {
Type itemType = ((ParameterizedType) type).getActualTypeArguments()[0];
return "a comma-delimited list where each item is " + registry.expectation(itemType);
}
@Override
public Object convert(String s, TypeConverterRegistry registry, Type t) {
final Type itemType = ((ParameterizedType) t).getActualTypeArguments()[0];
final Collection