| /* |
| * 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.JsonLocation; |
| import com.fasterxml.jackson.core.JsonParser; |
| import com.fasterxml.jackson.core.JsonToken; |
| import com.fasterxml.jackson.core.util.JsonParserSequence; |
| import com.fasterxml.jackson.databind.*; |
| import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory; |
| import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; |
| import com.fasterxml.jackson.databind.deser.DeserializerFactory; |
| import com.fasterxml.jackson.databind.deser.ResolvableDeserializer; |
| import com.fasterxml.jackson.databind.module.SimpleModule; |
| import com.fasterxml.jackson.databind.type.ArrayType; |
| import com.fasterxml.jackson.databind.type.CollectionLikeType; |
| import com.fasterxml.jackson.databind.type.CollectionType; |
| import com.fasterxml.jackson.databind.type.MapLikeType; |
| import com.fasterxml.jackson.databind.type.MapType; |
| import com.fasterxml.jackson.databind.type.ReferenceType; |
| import com.fasterxml.jackson.databind.util.TokenBuffer; |
| import com.google.common.annotations.Beta; |
| |
| import java.io.IOException; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.function.BiFunction; |
| import java.util.function.Function; |
| import org.apache.brooklyn.api.mgmt.ManagementContext; |
| import org.apache.brooklyn.util.collections.MutableList; |
| import org.apache.brooklyn.util.core.flags.TypeCoercions; |
| import org.apache.brooklyn.util.exceptions.Exceptions; |
| import org.apache.brooklyn.util.guava.Maybe; |
| import org.apache.brooklyn.util.javalang.Boxing; |
| import org.apache.brooklyn.util.text.Strings; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| public class BrooklynJacksonSerializationUtils { |
| |
| private static final Logger log = LoggerFactory.getLogger(BrooklynJacksonSerializationUtils.class); |
| |
| public static final String TYPE = "type"; |
| public static final String VALUE = "value"; |
| public static final String DEFAULT = "default"; |
| |
| |
| @Beta |
| public static JsonDeserializer<Object> createBeanDeserializer(DeserializationContext ctxt, JavaType t) throws JsonMappingException { |
| return createBeanDeserializer(ctxt, t, null, false, true); |
| } |
| |
| /** Do what ctxt.findRootValueDeserializer does, except don't get special things we've registered, so we can get actual bean deserializers back, |
| * e.g. as fallback impls in our custom deserializers */ |
| @Beta |
| public static JsonDeserializer<Object> createBeanDeserializer(DeserializationContext ctxt, JavaType t, BeanDescription optionalBeanDescription, |
| boolean beanFactoryBuildPossible, boolean resolve) throws JsonMappingException { |
| if (optionalBeanDescription==null) optionalBeanDescription = ctxt.getConfig().introspect(t); |
| |
| DeserializerFactory f = ctxt.getFactory(); |
| JsonDeserializer<Object> deser; |
| if (beanFactoryBuildPossible || f instanceof BeanDeserializerFactory) { |
| // go directly to builder to avoid returning ones based on annotations etc |
| deser = ((BeanDeserializerFactory)f).buildBeanDeserializer(ctxt, t, optionalBeanDescription); |
| } else { |
| deser = ctxt.getFactory().createBeanDeserializer(ctxt, t, optionalBeanDescription); |
| } |
| if (resolve && deser instanceof ResolvableDeserializer) { |
| ((ResolvableDeserializer) deser).resolve(ctxt); |
| } |
| return deser; |
| } |
| |
| static void dumpParser(String header, JsonParser p) { |
| try { |
| System.out.println(header); |
| while (true) { |
| System.out.println(p.getCurrentToken() + " - " + p.currentName() + " - " + p.getValueAsString()); |
| if (p.nextToken()==null) return; |
| } |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| /** per TokenBuffer.asCopyOfValue but if on a field _name_ it buffers all the key-values */ |
| @Beta |
| public static TokenBuffer createBufferForParserCurrentObject(JsonParser parser, DeserializationContext optionalCtxtForFeatures) throws IOException { |
| TokenBuffer pb = new TokenBuffer(parser, optionalCtxtForFeatures); |
| |
| while (parser.currentToken() == JsonToken.FIELD_NAME) { |
| // if on a field name (in an object), we want to buffer the entire object, so take key and value and continue (the initial start_object is fine not to be restored); |
| // needed for any deserializer which creates a token buffer inside an object, eg entity/selector predicate |
| // see: com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject |
| pb.copyCurrentStructure(parser); // copies name and value |
| parser.nextToken(); |
| } |
| |
| pb.copyCurrentStructure(parser); |
| |
| return pb; |
| } |
| |
| public static JsonParser createParserFromTokenBufferAndParser(TokenBuffer tb, JsonParser p) throws IOException { |
| JsonParserSequence pp = JsonParserSequence.createFlattened(false, tb.asParser(p), p); |
| pp.nextToken(); |
| return pp; |
| } |
| |
| public static Object readObject(DeserializationContext ctxt, JsonParser p) throws IOException { |
| // sometimes we do this, but it fails on trailing tokens |
| // p.readValueAs(Object.class) |
| if (p.currentToken()==JsonToken.END_OBJECT) return new Object(); |
| return ctxt.findRootValueDeserializer(ctxt.constructType(Object.class)).deserialize(p, ctxt); |
| } |
| |
| public static class ConfigurableBeanDeserializerModifier extends BeanDeserializerModifier { |
| public static boolean DEFAULT_APPLY_ONLY_TO_BEAN_DESERIALIZERS = false; |
| |
| List<Function<JsonDeserializer<?>,JsonDeserializer<?>>> deserializerWrappers = MutableList.of(); |
| List<Function<Object,Object>> postConstructFunctions = MutableList.of(); |
| public boolean applyOnlyToBeanDeserializers = DEFAULT_APPLY_ONLY_TO_BEAN_DESERIALIZERS; |
| |
| public ConfigurableBeanDeserializerModifier addDeserializerWrapper(Function<JsonDeserializer<?>,JsonDeserializer<?>> ...f) { |
| for (Function<JsonDeserializer<?>,JsonDeserializer<?>> fi: f) deserializerWrappers.add(fi); |
| return this; |
| } |
| |
| public ConfigurableBeanDeserializerModifier addPostConstructFunction(Function<Object,Object> f) { |
| postConstructFunctions.add(f); |
| return this; |
| } |
| |
| public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, |
| BeanDescription beanDesc, |
| JsonDeserializer<?> deserializer) { |
| return applyOurModifiers(deserializer); |
| } |
| |
| protected JsonDeserializer<?> applyOurModifiers(JsonDeserializer<?> deserializer) { |
| for (Function<JsonDeserializer<?>,JsonDeserializer<?>> d: deserializerWrappers) { |
| deserializer = d.apply(deserializer); |
| } |
| if (!postConstructFunctions.isEmpty()) { |
| deserializer = new JsonDeserializerInvokingPostConstruct(postConstructFunctions, deserializer); |
| } |
| return deserializer; |
| } |
| |
| private JsonDeserializer<?> applyOurModifiersNonBean(JsonDeserializer<?> deserializer) { |
| if (applyOnlyToBeanDeserializers) { |
| // all the super's callers just return the input, so it is safe to do this, ignoring the supers of the calling method |
| return deserializer; |
| } |
| return applyOurModifiers(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyMapDeserializer(DeserializationConfig config, MapType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyArrayDeserializer(DeserializationConfig config, ArrayType valueType, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyCollectionLikeDeserializer(DeserializationConfig config, CollectionLikeType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyEnumDeserializer(DeserializationConfig config, JavaType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| @Override |
| public JsonDeserializer<?> modifyReferenceDeserializer(DeserializationConfig config, ReferenceType type, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { |
| return applyOurModifiersNonBean(deserializer); |
| } |
| |
| // not supported at key because it is a different hierarchy; our deserializers probably don't apply; no known use case so NBD |
| // @Override |
| // public KeyDeserializer modifyKeyDeserializer(DeserializationConfig config, JavaType type, KeyDeserializer deserializer) { |
| // // super.modifyKeyDeserializer(config, type, deserializer); |
| // return applyOurModifiersNonBean(deserializer); |
| // } |
| |
| public <T extends ObjectMapper> T apply(T mapper) { |
| SimpleModule module = new SimpleModule(); |
| module.setDeserializerModifier(this); |
| return (T)mapper.registerModule(module); |
| } |
| } |
| |
| static class JsonDeserializerInvokingPostConstruct extends JacksonBetterDelegatingDeserializer { |
| final List<Function<Object,Object>> postConstructFunctions; |
| public JsonDeserializerInvokingPostConstruct(List<Function<Object,Object>> postConstructFunctions, JsonDeserializer<?> deserializer) { |
| super(deserializer, d -> new JsonDeserializerInvokingPostConstruct(postConstructFunctions, d)); |
| this.postConstructFunctions = postConstructFunctions; |
| } |
| |
| @Override |
| protected Object deserializeWrapper(JsonParser jp, DeserializationContext ctxt, BiFunctionThrowsIoException<JsonParser, DeserializationContext, Object> nestedDeserialize) throws IOException { |
| return postConstructFunctions.stream().reduce(Function::andThen).orElse(x -> x).apply( |
| nestedDeserialize.apply(jp, ctxt) ); |
| } |
| } |
| |
| public static class NestedLoggingDeserializer extends JacksonBetterDelegatingDeserializer { |
| private final StringBuilder prefix; |
| |
| public NestedLoggingDeserializer(StringBuilder prefix, JsonDeserializer<?> deserializer) { |
| super(deserializer, d -> new NestedLoggingDeserializer(prefix, d)); |
| this.prefix = prefix; |
| } |
| |
| @Override |
| protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) { |
| prefix.append("."); |
| try { |
| return constructor.apply(newDelegatee); |
| } finally { |
| prefix.setLength(prefix.length()-1); |
| } |
| } |
| |
| @Override |
| protected Object deserializeWrapper(JsonParser jp, DeserializationContext ctxt, BiFunctionThrowsIoException<JsonParser, DeserializationContext, Object> nestedDeserialize) throws IOException { |
| String v = jp.getCurrentToken()==JsonToken.VALUE_STRING ? jp.getValueAsString() : null; |
| try { |
| prefix.append(" "); |
| log.info(prefix+"> "+jp.getCurrentToken()); |
| Object result = nestedDeserialize.apply(jp, ctxt); |
| log.info(prefix+"< "+result); |
| return result; |
| } catch (Exception e) { |
| log.info(prefix+"< "+e); |
| throw e; |
| } finally { |
| prefix.setLength(prefix.length()-2); |
| } |
| } |
| } |
| |
| interface RecontextualDeserializer { |
| JsonDeserializer<?> recreateContextual(); |
| } |
| |
| public static class JsonDeserializerForCommonBrooklynThings extends JacksonBetterDelegatingDeserializer { |
| // injected from CAMP platform; inelegant, but effective |
| public static BiFunction<ManagementContext,Object,Object> BROOKLYN_PARSE_DSL_FUNCTION = null; |
| |
| private final ManagementContext mgmt; |
| public JsonDeserializerForCommonBrooklynThings(ManagementContext mgmt, JsonDeserializer<?> delagatee) { |
| super(delagatee, d -> new JsonDeserializerForCommonBrooklynThings(mgmt, d)); |
| this.mgmt = mgmt; |
| } |
| |
| @Override |
| protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) { |
| if (newDelegatee instanceof RecontextualDeserializer) { |
| newDelegatee = ((RecontextualDeserializer)newDelegatee).recreateContextual(); |
| } |
| return super.newDelegatingInstance(newDelegatee); |
| } |
| |
| @Override |
| public void resolve(DeserializationContext ctxt) throws JsonMappingException { |
| try { |
| super.resolve(ctxt); |
| } catch (JsonMappingException e) { |
| // supplying process or cause causes location to appear multiple times in message, |
| // so clumsy way to maintain a good message and the JsonMappingException type |
| // (though not sure we need to maintain that exception; we already lose subtypes |
| // eg InvalidDefinitionException, but nothing seems to mind) |
| throw (JsonMappingException) new JsonMappingException( |
| null, |
| (Strings.isBlank(e.getMessage()) ? e.toString() : e.getMessage()) + |
| (handledType()!=null ? ", processing "+handledType() : ""), |
| (JsonLocation)null).initCause(e); |
| } |
| } |
| |
| @Override |
| protected Object deserializeWrapper(JsonParser jp, DeserializationContext ctxt, BiFunctionThrowsIoException<JsonParser, DeserializationContext, Object> nestedDeserialize) throws IOException { |
| String v = jp.getCurrentToken()==JsonToken.VALUE_STRING ? jp.getValueAsString() : null; |
| try { |
| Object result = nestedDeserialize.apply(jp, ctxt); |
| |
| if (BROOKLYN_PARSE_DSL_FUNCTION!=null && mgmt!=null && result instanceof Map) { |
| Map<?, ?> rm = (Map<?, ?>) result; |
| if (Object.class.equals(_valueClass) || _valueClass==null) { |
| // this marker indicates that a DSL object was serialized and we need to re-parse it to deserialize it |
| Object brooklynLiteral = rm.get("$brooklyn:literal"); |
| if (brooklynLiteral != null) { |
| return BROOKLYN_PARSE_DSL_FUNCTION.apply(mgmt, brooklynLiteral); |
| } else { |
| Object type = rm.get("type"); |
| if (type instanceof String && ((String)type).startsWith("org.apache.brooklyn.camp.brooklyn.spi.dsl.")) { |
| // should always have a brooklyn:literal on outermost type, so that even with json pass through (used for steps etc) dsl gets preserved across convertDeeply |
| // apart from nested DSL expressions; and unf there is no easy way to tell here, so we just log debug either way |
| log.debug("Data "+result+" looks like DSL but has no literal entry; probably it is a nested DSL expression, or otherwise it may be left as a map after deserialization"); |
| } |
| } |
| } |
| } |
| |
| return result; |
| |
| } catch (Exception e) { |
| // if it fails, get the raw object and attempt a coercion?; currently just for strings |
| // we could do for maps but it would mean buffering every object, and it could cause recursive nightmares where the coercers tries a Jackson mapper |
| if ((String.class.equals(_valueClass) || Boxing.isPrimitiveOrBoxedClass(_valueClass)) && v==null && jp.getCurrentToken()==JsonToken.END_OBJECT) { |
| // primitives declaring just their type are allowed |
| try { |
| return _valueClass.getDeclaredConstructor().newInstance(); |
| } catch (Exception e2) { |
| Exceptions.propagateIfFatal(e2); |
| // ignore; use e instead |
| } |
| } |
| if (v!=null && handledType()!=null) { |
| // attempt type coercion |
| Maybe<?> coercion = TypeCoercions.tryCoerce(v, handledType()); |
| if (coercion.isPresent()) return coercion.get(); |
| } |
| if (e instanceof IOException) throw (IOException)e; |
| throw Exceptions.propagate(e); |
| } |
| } |
| } |
| |
| |
| } |