blob: a48870fd74b987dcf651b788074dda1ea2436f55 [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.core.resolve.jackson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.google.common.annotations.Beta;
import com.google.common.reflect.TypeToken;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Predicate;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.api.mgmt.classloading.BrooklynClassLoadingContext;
import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.core.mgmt.classloading.OsgiBrooklynClassLoadingContext;
import org.apache.brooklyn.core.resolve.jackson.BrooklynJacksonSerializationUtils.ConfigurableBeanDeserializerModifier;
import org.apache.brooklyn.core.resolve.jackson.BrooklynJacksonSerializationUtils.JsonDeserializerForCommonBrooklynThings;
import org.apache.brooklyn.core.typereg.RegisteredTypes;
import org.apache.brooklyn.util.core.task.DeferredSupplier;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.guava.TypeTokens;
import org.apache.brooklyn.util.javalang.Boxing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BeanWithTypeUtils {
private static final Logger LOG = LoggerFactory.getLogger(BeanWithTypeUtils.class);
public static final String FORMAT = "bean-with-type";
/** also see {@link org.apache.brooklyn.util.core.json.BrooklynObjectsJsonMapper#newMapper(ManagementContext)}
* which isn't as powerful in most ways, but has a few extra things it supports
* TODO ideally that and this would be combined */
public static ObjectMapper newMapper(ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowPojoJavaTypes) {
return applyCommonMapperConfig(newSimpleMapper(), mgmt, allowRegisteredTypes, loader, allowPojoJavaTypes);
}
public static ObjectMapper newYamlMapper(ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowPojoJavaTypes) {
return newYamlMapper(mgmt, allowRegisteredTypes, loader, allowPojoJavaTypes, false);
}
public static ObjectMapper newYamlMapper(ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowPojoJavaTypes, boolean allowYamlTagsForType) {
ObjectMapper mapper = applyCommonMapperConfig(newSimpleYamlMapper(allowYamlTagsForType), mgmt, allowRegisteredTypes, loader, allowPojoJavaTypes);
mapper = mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
public static ObjectMapper applyCommonMapperConfig(ObjectMapper mapper, ManagementContext mgmt, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowPojoJavaTypes) {
mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
BrooklynRegisteredTypeJacksonSerialization.apply(mapper, mgmt, allowRegisteredTypes, loader, allowPojoJavaTypes);
WrappedValuesSerialization.apply(mapper, mgmt);
mapper = new ConfigurableBeanDeserializerModifier()
.addDeserializerWrapper(
d -> new JsonDeserializerForCommonBrooklynThings(mgmt, d)
// see note below, on convert()
).apply(mapper);
CommonTypesSerialization.apply(mapper, mgmt);
return mapper;
}
public static JsonMapper newSimpleMapper() {
// for use with json maps (no special type resolution, even the field "type" is ignored)
return JsonMapper.builder()
// experimented with this, but safer for us to add annotations, and for eg guava it doesn't work due to no parameter names in the compiled code
//.constructorDetector(ConstructorDetector.USE_DELEGATING)
.build();
}
public static YAMLMapper newSimpleYamlMapper() {
return newSimpleYamlMapper(false);
}
public static YAMLMapper newSimpleYamlMapper(boolean allowYamlTagsForType) {
// for use with json maps (no special type resolution, even the field "type" is ignored);
// do not split lines as that makes output harder to read
YAMLMapper result = YAMLMapper.builder().build()
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
.enable(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS) //otherwise "1" becomes 1
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.disable(YAMLGenerator.Feature.SPLIT_LINES);
if (!allowYamlTagsForType) {
// whether to use !<type> tags; normally not, use the same type / (type) keys as done for json
// if we know we will be deserializing it using one of these jackson deserializers, it could be enabled,
// though not sure if there is any point. note that snakeyaml does not support custom types by default.
result.disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID);
}
return result;
}
public static boolean isPureJson(Object o) {
return isJsonAndOthers(o, oo -> false);
}
public static boolean isJsonAndOthers(Object o, Predicate<Object> acceptOthers) {
if (o==null || Boxing.isPrimitiveOrStringOrBoxedObject(o)) return true;
if (o instanceof Collection) {
for (Object oo : (Collection<?>) o) {
if (!isJsonAndOthers(oo, acceptOthers)) return false;
}
return true;
}
if (o instanceof Map) {
for (Map.Entry<?,?> oo : ((Map<?,?>) o).entrySet()) {
if (!isJsonAndOthers(oo.getKey(), acceptOthers)) return false;
if (!isJsonAndOthers(oo.getValue(), acceptOthers)) return false;
}
return true;
}
return acceptOthers.test(o);
}
public static boolean isJsonOrDeferredSupplier(Object o) {
return isJsonAndOthers(o, oo -> oo instanceof DeferredSupplier);
}
/* a lot of consideration over where bean-with-type conversion should take place.
* it is especially handy for config and for initializers, and sometimes for values _within_ those items.
* currently these are done:
* - in BrooklynComponentTemplateResolver for config, initializers etc
* - on coercion, when accessing config
* see esp CustomTypeConfigYamlTest.
*
* BrooklynComponentTemplateResolver is also responsible for parsing the DSL, which it does first,
* and DSL objects are preserved by bean-with-type transformation --
* see in JsonDeserializerForCommonBrooklynThings. See DslSerializationTest.
*/
static ThreadLocal<Stack<Object>> activeConversions = new ThreadLocal<>();
public static <T> T convert(ManagementContext mgmt, Object mapOrListToSerializeThenDeserialize, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes) throws JsonProcessingException {
Stack<Object> stack = activeConversions.get();
if (stack==null) {
stack = new Stack<>();
activeConversions.set(stack);
}
// simple things, like string, might be converted by a JsonDeserializer or by other TypeCoercions;
// so the former calls the latter, and the latter calls the former. but stop at some point!
if (stack.contains(mapOrListToSerializeThenDeserialize)) throw new IllegalStateException("Aborting recursive attempt to convert '"+mapOrListToSerializeThenDeserialize+"'");
T wrongTypeResult = null;
try {
stack.push(mapOrListToSerializeThenDeserialize);
// prefer this because (a) it's cheaper, and (b) it supports deferred values more nicely;
// ObjectReferencingSerialization.deserializeWrapper will do type coercion so very few things if any should need deep coercion now
T result = convertShallow(mgmt, mapOrListToSerializeThenDeserialize, type, allowRegisteredTypes, loader, allowJavaTypes);
// T result2 = null;
// try {
// result2 = convertDeeply(mgmt, mapOrListToSerializeThenDeserialize, type, allowRegisteredTypes, loader, allowJavaTypes);
// } catch (Exception e2) {
// Exceptions.propagateIfFatal(e2);
// // otherwise ignore
// }
// if (!Objects.equals(result, result2)) {
// // legacy preferred convert deeply; in a few places this mattered.
// // need to investigate when/why
// return result2;
// }
if (result!=null && !type.getRawType().isInstance(result)) {
wrongTypeResult = result;
throw new IllegalStateException("Wrong type returned"); // will be ignored below
} else {
return result;
}
} catch (Exception e) {
try {
// needed for a few things, mainly where a bean has a type field that conflicts with the type here,
// tryCoercer -20-wrong-bean uses this
T result = convertDeeply(mgmt, mapOrListToSerializeThenDeserialize, type, allowRegisteredTypes, loader, allowJavaTypes);
if (wrongTypeResult!=null) {
if (result==null || !type.getRawType().isInstance(result)) {
LOG.warn("Wrong type returned coercing "+mapOrListToSerializeThenDeserialize+" to "+type+"; got "+wrongTypeResult+" / "+result);
// prefer the original if both get it wrong
return wrongTypeResult;
} else {
LOG.warn("Wrong type returned coercing "+mapOrListToSerializeThenDeserialize+" to "+type+" shallow, but succeeded on deep; got "+wrongTypeResult+" / "+result);
return result;
}
}
return result;
} catch (Exception e2) {
Exceptions.propagateIfFatal(e2);
if (wrongTypeResult!=null) return wrongTypeResult;
throw Exceptions.propagate(Arrays.asList(e, e2));
}
} finally {
stack.pop();
if (stack.isEmpty()) activeConversions.remove();
}
}
@Beta
public static <T> T convertShallow(ManagementContext mgmt, Object mapOrListToSerializeThenDeserialize, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes) throws JsonProcessingException {
// try with complex types are saved as objects rather than serialized; might not work if special deserialization is wanted to apply to a map inside a complex type,
// but type coercions might mean that it does actually work but doing a nested convert shallow
ObjectMapper mapper = YAMLMapper.builder().build();
mapper = BeanWithTypeUtils.applyCommonMapperConfig(mapper, mgmt, allowRegisteredTypes, loader, allowJavaTypes);
mapper = new ObjectReferencingSerialization().useAndApplytoMapper(mapper);
String serialization = type.getRawType().equals(Object.class) ? mapper.writeValueAsString(mapOrListToSerializeThenDeserialize) : mapper.writerFor(Object.class).writeValueAsString(mapOrListToSerializeThenDeserialize);
return mapper.readValue(serialization, BrooklynJacksonType.asJavaType(mapper, type));
}
@Beta
public static <T> T convertDeeply(ManagementContext mgmt, Object mapOrListToSerializeThenDeserialize, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes) throws JsonProcessingException {
// try full serialization - but won't work if things being written cannot be deserialized, eg due to unknown type
ObjectMapper mapper = newMapper(mgmt, allowRegisteredTypes, loader, allowJavaTypes);
boolean useLonghandObjectWriter = true;
if (type.getRawType().equals(Object.class)) useLonghandObjectWriter = false;
else if (mapOrListToSerializeThenDeserialize==null || Boxing.isPrimitiveOrStringOrBoxedObject(mapOrListToSerializeThenDeserialize)) useLonghandObjectWriter = false;
String serialization = !useLonghandObjectWriter ? mapper.writeValueAsString(mapOrListToSerializeThenDeserialize) : mapper.writerFor(Object.class).writeValueAsString(mapOrListToSerializeThenDeserialize);
return mapper.readValue(serialization, BrooklynJacksonType.asJavaType(mapper, type));
}
public static <T> Maybe<T> tryConvertOrAbsentUsingContext(Maybe<Object> input, TypeToken<T> type) {
return tryConvertOrAbsentUsingContext(input, type, false);
}
public static <T> Maybe<T> tryConvertOrAbsentUsingContext(Maybe<Object> input, TypeToken<T> type, boolean allowNonMapComplexInput) {
Entity entity = BrooklynTaskTags.getContextEntity(Tasks.current());
ManagementContext mgmt = entity != null ? ((EntityInternal) entity).getManagementContext() : null;
BrooklynClassLoadingContext loader = entity != null ? RegisteredTypes.getClassLoadingContext(entity) : null;
return BeanWithTypeUtils.tryConvertOrAbsent(mgmt, input, type, true, loader, true, allowNonMapComplexInput);
}
public static <T> Maybe<T> tryConvertOrAbsent(ManagementContext mgmt, Maybe<Object> inputMap, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes) {
return tryConvertOrAbsent(mgmt, inputMap, type, allowRegisteredTypes, loader, allowJavaTypes, false);
}
public static <T> Maybe<T> tryConvertOrAbsent(ManagementContext mgmt, Maybe<Object> inputMap, TypeToken<T> type, boolean allowRegisteredTypes, BrooklynClassLoadingContext loader, boolean allowJavaTypes, boolean allowNonMapComplexInput) {
if (inputMap.isAbsent()) return (Maybe<T>)inputMap;
Object o = inputMap.get();
if (!(o instanceof Map) && !(o instanceof List) && !Boxing.isPrimitiveOrStringOrBoxedObject(o)) {
if (type.isSupertypeOf(o.getClass())) {
return (Maybe<T>)inputMap;
} else {
if (!allowNonMapComplexInput) {
return Maybe.absent(() -> new RuntimeException("BeanWithType cannot convert from " + o.getClass() + " to " + type));
} // else continue below
}
}
Maybe<T> fallback = null;
if (!BrooklynJacksonType.isRegisteredType(type)) {
if (type.isSupertypeOf(Object.class)) {
// the input is already valid, so use it as the fallback result
fallback = (Maybe<T>) inputMap;
// there isn't a 'type' key so little obvious point in converting .. might make a difference _inside_ a map or list, but we've not got any generics so it won't
if (!(o instanceof Map) || !((Map<?, ?>) o).containsKey(BrooklynJacksonSerializationUtils.TYPE)) return fallback;
} else if (type.isSupertypeOf(Map.class) && o instanceof Map) {
// skip conversion for a map if it isn't an object
return (Maybe<T>) inputMap;
}
}
try {
return Maybe.ofDisallowingNull(convert(mgmt, o, type, allowRegisteredTypes, loader, allowJavaTypes));
} catch (Exception e) {
if (fallback!=null) return fallback;
return Maybe.absent("BeanWithType cannot convert given input "+o+" to "+type, e);
}
}
/** Whether there appears to be an incompatibility and conversion might fix it. */
public static <T> boolean isConversionRecommended(Maybe<Object> input, TypeToken<T> type) {
return getPotentialConvertibilityScoreInternal(input.orNull(), type) == 1;
}
/** Like {@link #isConversionRecommended(Maybe, TypeToken)} but much weaker,
* in particular if Object is expected, this will return true whereas that will return false.
* This will return false if there are contents which are simply incompatible. */
public static <T> boolean isConversionPlausible(Maybe<Object> input, TypeToken<T> type) {
if (input==null || input.isAbsentOrNull() || type==null) return false;
return getPotentialConvertibilityScoreInternal(input.get(), type) >= 0;
}
/** -1 if conversion _won't_ fix it. 1 if there is a problem that likely _would_ be fixed. 0 if no obvious need or problem. */
private static <T> int getPotentialConvertibilityScoreInternal(Object t, TypeToken<T> type) {
// if we want a primitive/string, conversion won't help (coercion would be sufficient); return 0 if type is consistent, or -1 if not
if (t==null) {
return 0;
}
if (Boxing.isPrimitiveOrBoxedClass(TypeTokens.getRawRawType(type)) || TypeTokens.equalsRaw(String.class, type)) {
return TypeTokens.equalsRaw(t.getClass(), type) ? 0 : -1;
}
// if we want an object, then no need for conversion nor any problem with conversion
if (TypeTokens.equalsRaw(Object.class, type)) return 0;
if (TypeTokens.isAssignableFromRaw(Map.class, type)) {
// if map is wanting, superficially we don't want to convert; but if there are generics we need to recurse
if (t instanceof Map) {
List<TypeToken<?>> generics = TypeTokens.getGenericArguments(type);
if (generics!=null) {
for (Entry<?, ?> entry : ((Map<?, ?>) t).entrySet()) {
int k = getPotentialConvertibilityScoreInternal(entry.getKey(), generics.get(0));
if (k!=0) return k;
int v = getPotentialConvertibilityScoreInternal(entry.getValue(), generics.get(1));
if (v!=0) return v;
}
}
return 0;
} else {
// conversion won't make a map from a non-map
return -1;
}
}
if (TypeTokens.isAssignableFromRaw(Collection.class, type)) {
if (t instanceof Collection) {
List<TypeToken<?>> generics = TypeTokens.getGenericArguments(type);
if (generics!=null) {
for (Object entry : ((Collection<?>) t)) {
int k = getPotentialConvertibilityScoreInternal(entry, generics.get(0));
if (k != 0) return k;
}
}
return 0;
} else {
return -1;
}
}
// we want some special object. if we have a map or a string or possibly a primitive then conversion might sort us out.
return (t instanceof Map || Boxing.isPrimitiveOrStringOrBoxedObject(t)) ? 1 : -1;
}
}