blob: fe5ec0ca4932f9d33ced13b8ee66187f8144c1a1 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.brooklyn.util.javalang.coerce;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import com.google.common.annotations.Beta;
import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.reflect.TypeToken;
import org.apache.brooklyn.core.validation.BrooklynValidation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.AnyExceptionSupplier;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.guava.TypeTokens;
import org.apache.brooklyn.util.javalang.Boxing;
import org.apache.brooklyn.util.javalang.Reflections;
import org.apache.brooklyn.util.text.NaturalOrderComparator;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Attempts to coerce {@code value} to {@code targetType}.
* <p>
* Maintains a registry of adapter functions for type pairs in a {@link Table} which
* is searched after checking various strategies, including the following:
* <ul>
* <li>{@code value.asTargetType()}
* <li>{@code TargetType.fromType(value)} (if {@code value instanceof Type})
* <li>{@code value.targetTypeValue()} (handy for primitives)
* <li>{@code TargetType.valueOf(value)} (for enums)
* </ul>
* <p>
* A default set of adapters will handle most common Java-type coercions
* as well as <code>String</code> coercion to:
* <ul>
* <li> {@link Set}, {@link List}, {@link Map} and similar -- parses as YAML
* <li> {@link Date} -- parses using {@link Time#parseDate(String)}
* <li> {@link Duration} -- parses using {@link Duration#parse(String)}
* </ul>
*/
public class TypeCoercerExtensible implements TypeCoercer {
private static final Logger log = LoggerFactory.getLogger(TypeCoercerExtensible.class);
protected TypeCoercerExtensible() {}
/** has all the strategies (primitives, collections, etc)
* and all the adapters from {@link CommonAdaptorTypeCoercions} */
public static TypeCoercerExtensible newDefault() {
TypeCoercerExtensible result = newEmpty();
new CommonAdaptorTypeCoercions(result).registerAllAdapters();
new CommonAdaptorTryCoercions(result).registerAllAdapters();
return result;
}
/** has all the strategies (primitives, collections, etc) but no adapters,
* so caller can pick and choose e.g. from {@link CommonAdaptorTypeCoercions} */
public static TypeCoercerExtensible newEmpty() {
return new TypeCoercerExtensible();
}
/** Store the coercion {@link Function functions} in a {@link Table table}. */
private Table<Class<?>, Class<?>, Function<?,?>> registry = HashBasedTable.create();
/** Store the generic coercers, ordered by the name; reset each time updated */
private SortedMap<String,TryCoercer> genericCoercersByName = Maps.newTreeMap(NaturalOrderComparator.INSTANCE);
@Override
public <T> T coerce(Object value, Class<T> targetType) {
return coerce(value, TypeToken.of(targetType));
}
public <T> T coerce(Object value, TypeToken<T> targetTypeToken) {
return tryCoerce(value, targetTypeToken).get();
}
@Override
public <T> Maybe<T> tryCoerce(Object input, Class<T> type) {
return changeExceptionSupplier( tryCoerceInternal(input, null, type) );
}
@Override
public <T> Maybe<T> tryCoerce(Object value, TypeToken<T> targetTypeToken) {
return changeExceptionSupplier( tryCoerceInternal(value, targetTypeToken, null) );
}
protected <T> Maybe<T> changeExceptionSupplier(Maybe<T> result) {
return Maybe.Absent.changeExceptionSupplier(result, ClassCoercionException.class);
}
@SuppressWarnings("unchecked")
protected <T> Maybe<T> tryCoerceInternal(Object value, TypeToken<T> targetTypeToken, Class<T> targetType) {
return tryCoerceInternal2(value, targetTypeToken, targetType).map(BrooklynValidation.getInstance()::ensureValid);
}
protected <T> Maybe<T> tryCoerceInternal2(Object value, TypeToken<T> targetTypeToken, Class<T> targetType) {
if (value==null) return Maybe.of((T)null);
Maybe<T> result = null;
List<Maybe<T>> errors = MutableList.of();
//recursive coercion of parameterized collections and map entries
targetType = TypeTokens.getRawType(targetTypeToken, targetType);
if (targetTypeToken!=null && targetTypeToken.getType() instanceof ParameterizedType) {
if (value instanceof Iterable && Iterable.class.isAssignableFrom(targetType)) {
result = tryCoerceIterable(value, targetTypeToken, targetType);
} else if (value.getClass().isArray() && Iterable.class.isAssignableFrom(targetType)) {
result = tryCoerceArray(value, targetTypeToken, targetType);
} else if (value instanceof Map && Map.class.isAssignableFrom(targetType)) {
result = tryCoerceMap(value, targetTypeToken);
}
if (result!=null) {
if (result.isPresent()) return result;
// Previous to v1.0.0 we'd overlook errors in generics with warnings; now we bail
TypeToken<T> targetTypeTokenF = targetTypeToken;
RuntimeException e = Maybe.getException(result);
return Maybe.absent(new AnyExceptionSupplier<>(ClassCoercionException.class,
() -> "Generic type mismatch coercing "+value.getClass().getName()+" to "+targetTypeTokenF+": "+Exceptions.collapseText(e), e));
}
}
if (targetType.isInstance(value)) return Maybe.of( (T) value );
targetTypeToken = TypeTokens.getTypeToken(targetTypeToken, targetType);
for (Entry<String, TryCoercer> mapEntry : genericCoercersByName.entrySet()) {
String coercerName = mapEntry.getKey();
if (coercerName != null && !coercerName.startsWith("-")) {
Maybe<T> resultM = applyCoercer(value, targetTypeToken, errors, mapEntry.getValue(), coercerName);
if (resultM != null) return resultM;
}
}
//ENHANCEMENT could look in type hierarchy of both types for a conversion method...
//at this point, if either is primitive then run instead over boxed types
Class<?> boxedT = Boxing.PRIMITIVE_TO_BOXED.get(targetType);
Class<?> boxedVT = Boxing.PRIMITIVE_TO_BOXED.get(value.getClass());
if (boxedT!=null || boxedVT!=null) {
try {
if (boxedT==null) boxedT=targetType;
Object boxedV = boxedVT==null ? value : boxedVT.getConstructor(value.getClass()).newInstance(value);
return tryCoerce(boxedV, (Class<T>)boxedT);
} catch (Exception e) {
return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass()+" to "+targetType.getCanonicalName()+" ("+value+"): unboxing failed", e));
}
}
//now look in registry
//previously synched on registry; but now we make the registry immutable
Map<Class<?>, Function<?,?>> adapters = registry.row(targetType);
for (Map.Entry<Class<?>, Function<?,?>> entry : adapters.entrySet()) {
if (entry.getKey().isInstance(value)) {
try {
T resultT = ((Function<Object,T>)entry.getValue()).apply(value);
// Check if need to unwrap again (e.g. if want List<Integer> and are given a String "1,2,3"
// then we'll have so far converted to List.of("1", "2", "3"). Call recursively.
// First check that value has changed, to avoid stack overflow!
if (!Objects.equal(value, resultT) && targetTypeToken.getType() instanceof ParameterizedType) {
// Could duplicate check for `result instanceof Collection` etc; but recursive call
// will be fine as if that doesn't match we'll safely reach `targetType.isInstance(value)`
// and just return the result.
Maybe<T> resultM = tryCoerce(resultT, targetTypeToken);
if (resultM!=null) {
if (resultM.isPresent()) return resultM;
// if couldn't coerce parameterized types then back out of this coercer
// but remember the error if we were first
errors.add(resultM);
}
} else {
return Maybe.of(resultT);
}
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
if (log.isDebugEnabled()) {
log.debug("When coercing, registry adapter "+entry+" gave error on "+value+" -> "+targetType+" "
+ (errors.isEmpty() ? "(rethrowing)" : "(adding as secondary error as there is already another)")
+ ": "+e, e);
}
if (e instanceof ClassCoercionException) {
errors.add(Maybe.absent(e));
} else {
errors.add(Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetTypeToken+" ("+value+"): registered coercer failed", e)));
}
continue;
}
}
}
// now try negative ordered coercers
for (Entry<String, TryCoercer> mapEntry : genericCoercersByName.entrySet()) {
String coercerName = mapEntry.getKey();
if (coercerName != null && coercerName.startsWith("-")) {
Maybe<T> resultM = applyCoercer(value, targetTypeToken, errors, mapEntry.getValue(), coercerName);
if (resultM != null) return resultM;
}
}
// not found
if (!errors.isEmpty()) {
if (errors.size()==1) return Iterables.getOnlyElement(errors);
return Maybe.absent(Exceptions.create(errors.stream().map(Maybe.Absent::getException).collect(Collectors.toList())));
}
if (value instanceof Map) {
if (((Map)value).containsKey("type")) {
return Maybe.absent(new ClassCoercionException("Cannot coerce map containing {type: \""+((Map)value).get("type")+"\"} to "+targetTypeToken+": type not known or not supported here"));
}
return Maybe.absent(new ClassCoercionException("Cannot coerce map to "+targetTypeToken+" ("+value+"): no adapter known"));
}
return Maybe.absent(new ClassCoercionException("Cannot coerce type "+value.getClass().getCanonicalName()+" to "+targetTypeToken+" ("+value+"): no adapter known"));
}
private <T> Maybe<T> applyCoercer(Object value, TypeToken<T> targetTypeToken, List<Maybe<T>> errors, TryCoercer coercer, String coercerName) {
Maybe<T> result;
result = coercer.tryCoerce(value, targetTypeToken);
if (result!=null && result.isPresentAndNonNull()) {
// Check if need to unwrap again (e.g. if want List<Integer> and are given a String "1,2,3"
// then we'll have so far converted to List.of("1", "2", "3"). Call recursively.
// First check that value has changed, to avoid stack overflow!
if (!Objects.equal(value, result.get()) && !Objects.equal(value.getClass(), result.get().getClass())
// previously did this just for generics but it's more useful than that, e.g. if was a WrappedValue
//&& targetTypeToken.getType() instanceof ParameterizedType
) {
Maybe<T> resultM = tryCoerce(result.get(), targetTypeToken);
if (resultM!=null) {
if (resultM.isPresent()) return resultM;
// if couldn't coerce parameterized types then back out of this coercer
result = resultM;
}
} else {
return result;
}
}
if (result!=null) {
if (result.isAbsent()) errors.add(result);
else {
if (coercer instanceof TryCoercer.TryCoercerReturningNull) {
return result;
} else {
log.warn("Coercer " + coercerName + " returned wrapped null when coercing " + value);
errors.add(Maybe.absent("coercion returned null ("+coercerName+")"));
// coercers that return null should implement 'TryCoercerReturningNull'
}
}
}
return null;
}
@SuppressWarnings("unchecked")
protected <T> Maybe<T> tryCoerceMap(Object value, TypeToken<T> targetTypeToken) {
if (!(value instanceof Map) || !(TypeTokens.isAssignableFromRaw(Map.class, targetTypeToken))) return null;
Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments();
if (arguments.length != 2) {
throw new IllegalStateException("Unexpected number of parameters in map type: " + arguments);
}
Map<Object,Object> coerced = Maps.newLinkedHashMap();
TypeToken<?> mapKeyType = TypeToken.of(arguments[0]);
TypeToken<?> mapValueType = TypeToken.of(arguments[1]);
int i=0;
for (Map.Entry<?,?> entry : ((Map<?,?>) value).entrySet()) {
Maybe<?> k = tryCoerce(entry.getKey(), mapKeyType);
if (k.isAbsent()) return Maybe.absent(new ClassCoercionException(
"Could not coerce key of entry "+i+" ("+entry.getKey()+") to "+mapKeyType+" in "+targetTypeToken,
((Maybe.Absent<T>)k).getException()));
Maybe<?> v = tryCoerce(entry.getValue(), mapValueType);
if (v.isAbsent()) return Maybe.absent(new ClassCoercionException(
"Could not coerce value of entry "+i+" ("+entry.getValue()+") to "+mapValueType+" in "+targetTypeToken,
((Maybe.Absent<T>)v).getException()));
coerced.put(k.get(), v.get());
i++;
}
return Maybe.of((T) Maps.newLinkedHashMap(coerced));
}
protected <T> Maybe<T> tryCoerceArray(Object value, TypeToken<T> targetTypeToken, Class<? super T> targetType) {
List<?> listValue = Reflections.arrayToList(value);
return tryCoerceIterable(listValue, targetTypeToken, targetType);
}
/** tries to coerce a list;
* returns null if it just doesn't apply, a {@link Maybe.Present} if it succeeded,
* or {@link Maybe.Absent} with a good exception if it should have applied but couldn't */
@SuppressWarnings("unchecked")
protected <T> Maybe<T> tryCoerceIterable(Object value, TypeToken<T> targetTypeToken, Class<? super T> targetType) {
if (!(value instanceof Iterable) || !(TypeTokens.isAssignableFromRaw(Iterable.class, targetTypeToken))) return null;
Type[] arguments = ((ParameterizedType) targetTypeToken.getType()).getActualTypeArguments();
if (arguments.length != 1) {
return Maybe.absent(new IllegalStateException("Unexpected number of parameters in iterable type: " + arguments));
}
Collection<Object> coerced = Lists.newLinkedList();
TypeToken<?> listEntryType = TypeToken.of(arguments[0]);
int i = 0;
for (Object entry : (Iterable<?>) value) {
Maybe<?> entryCoerced = tryCoerce(entry, listEntryType);
if (entryCoerced.isPresent()) {
coerced.add(entryCoerced.get());
} else {
return Maybe.absent(new ClassCoercionException(
"Could not coerce entry "+i+" ("+entry+") to "+listEntryType,
((Maybe.Absent<T>)entryCoerced).getException()));
}
i++;
}
if (Set.class.isAssignableFrom(targetType)) {
return Maybe.of((T) Sets.newLinkedHashSet(coerced));
} else {
return Maybe.of((T) Lists.newArrayList(coerced));
}
}
/**
* Returns a function that does a type coercion to the given type. For example,
* {@code TypeCoercions.function(Double.class)} will return a function that will
* coerce its input value to a {@link Double} (or throw a {@link ClassCoercionException}
* if that is not possible).
*/
public <T> Function<Object, T> function(final Class<T> type) {
return new CoerceFunctionals.CoerceFunction<T>(this, type);
}
/** Registers an adapter for use with type coercion. Returns any old adapter registered for this pair. */
@SuppressWarnings("unchecked")
public synchronized <A,B> Function<? super A,B> registerAdapter(Class<A> sourceType, Class<B> targetType, Function<? super A,B> fn) {
HashBasedTable<Class<?>, Class<?>, Function<?, ?>> newRegistry = HashBasedTable.create(registry);
Function<? super A, B> result = (Function<? super A, B>) newRegistry.put(targetType, sourceType, fn);
registry = newRegistry;
return result;
}
/** Registers a generic adapter for use with type coercion. */
@Beta
public synchronized void registerAdapter(String nameAndOrder, TryCoercer fn) {
TreeMap<String, TryCoercer> gcn = Maps.newTreeMap(genericCoercersByName);
gcn.put(nameAndOrder, fn);
genericCoercersByName = gcn;
}
/** @deprecated since introduction, use {@link #registerAdapter(String, TryCoercer)} */
@Beta @Deprecated
public void registerAdapter(TryCoercer fn) {
registerAdapter(Time.makeDateStampString()+"-"+Strings.makePaddedString(""+(genericCoercersByName.size()), 3, "0", ""), fn);
}
}