Java tutorial
/* $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ * * @license * 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 io.coala.json; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import javax.inject.Provider; import org.aeonbits.owner.Accessible; import org.aeonbits.owner.Config; import org.aeonbits.owner.ConfigFactory; import org.aeonbits.owner.Mutable; import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.node.ValueNode; import io.coala.exception.ExceptionFactory; import io.coala.exception.Thrower; import io.coala.log.LogUtil; import io.coala.util.ReflectUtil; import io.coala.util.TypeArguments; /** * {@link DynaBean} implements a dynamic bean, ready for JSON de/serialization * * @version $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ * @author Rick van Krevelen */ @SuppressWarnings("rawtypes") @JsonInclude(Include.NON_NULL) public final class DynaBean implements Cloneable, Comparable { /** * {@link BeanProxy} is a annotation used to recognize {@link DynaBean} * entities/tags during de/serialization and specify the property to use for * {@link Comparable}s * * @version $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ * @author Rick van Krevelen */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface BeanProxy { /** * @return */ String comparableOn() default ""; } /** */ private static final Logger LOG = LogUtil.getLogger(DynaBean.class); /** leave null as long as possible */ @JsonIgnore private Map<String, Object> dynamicProperties = null; /** * {@link DynaBean} zero-arg bean constructor for (de)serialization */ @JsonCreator protected DynaBean() { // empty } protected void lock() { if (this.dynamicProperties != null) this.dynamicProperties = Collections.unmodifiableMap(this.dynamicProperties); } /** * @return the map of property values */ @JsonAnyGetter protected Map<String, Object> any() { return this.dynamicProperties == null ? Collections.emptyMap() : this.dynamicProperties; } /** * @param key * @return */ public boolean has(final String key) { return any().containsKey(key); } /** * @param key * @return */ public boolean hasNonNull(final String key) { final Object value = get(key); return value != null; } /** * @param key * @param value * @return {@code true} iff this bean contains the specified {@code value} * at specified {@code key}, i.e. both null/empty or both equal */ public boolean match(final String key, final Object value) { final Object v = get(key); return value == null ? v == null : value.equals(v); } /** * @param key * @return */ public Object get(final String key) { return any().get(key); } /** * helper-method * * @param key * @param defaultValue * @return the dynamically set value, or {@code defaultValue} if not set */ @SuppressWarnings("unchecked") protected <T> T get(final String key, final T defaultValue) { final Object result = get(key); return result == null ? defaultValue : (T) result; } /** * helper-method * * @param key * @param returnType * @return the currently set value, or {@code null} if not set */ @SuppressWarnings("unchecked") protected <T> T get(final String key, final Class<T> returnType) { return (T) get(key); } private Map<String, Object> getOrCreateMap() { if (this.dynamicProperties == null) this.dynamicProperties = new TreeMap<String, Object>(); return this.dynamicProperties; } protected void set(final Map<String, Object> values) { Map<String, Object> map = getOrCreateMap(); synchronized (map) { map.putAll(values); } } @JsonAnySetter protected Object set(final String key, final Object value) { Map<String, Object> map = getOrCreateMap(); synchronized (map) { return map.put(key, value); } } protected Object remove(final String key) { Map<String, Object> map = getOrCreateMap(); synchronized (map) { return map.remove(key); } } @SuppressWarnings("unchecked") @Override protected DynaBean clone() { final Map<String, Object> values = any(); final DynaBean result = new DynaBean(); result.set(JsonUtil.valueOf(JsonUtil.toTree(values), values.getClass())); return result; } @Override public int hashCode() { return any().hashCode(); } @Override public boolean equals(final Object other) { return any().equals(other); } @Override public int compareTo(final Object o) { throw new IllegalStateException("Invocation should be intercepted"); } @Override public String toString() { try { return JsonUtil.getJOM().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS).writeValueAsString(any()); } catch (final IOException e) { LOG.warn("Problem serializing " + getClass().getName(), e); return super.toString(); } } /** cache of type arguments for known {@link Identifier} sub-types */ // static final Map<Class<?>, Provider<?>> DYNABEAN_PROVIDER_CACHE = new // WeakHashMap<>(); /** * {@link DynaBeanInvocationHandler} * * @version $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ * @author Rick van Krevelen */ static class DynaBeanInvocationHandler implements InvocationHandler { /** */ private static final Logger LOG = LogUtil.getLogger(DynaBeanInvocationHandler.class); /** */ private final Class<?> type; /** */ private final Config config; /** */ protected final DynaBean bean; /** * {@link DynaBeanInvocationHandler} constructor */ public DynaBeanInvocationHandler(final ObjectMapper om, final Class<?> type, final DynaBean bean, final Properties... imports) { this.type = type; this.bean = bean; // LOG.trace("Using imports: " + Arrays.asList(imports)); Config config = null; if (Config.class.isAssignableFrom(type)) { // always create fresh, never from cache config = ConfigFactory.create(type.asSubclass(Config.class), imports); if (Mutable.class.isAssignableFrom(type)) { final Mutable mutable = (Mutable) config; mutable.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(final PropertyChangeEvent change) { LOG.trace("{} changed: {} = {} (was {})", type.getSimpleName(), change.getPropertyName(), change.getNewValue(), change.getOldValue()); // remove bean property in favor of changed // default config // bean.remove(change.getPropertyName()); /* * TODO parse actual value into bean try { * final Method method = * type.getMethod(change * .getPropertyName()); final Object * newValue = om.readValue( * change.getNewValue().toString(), * JsonUtil.checkRegistered(om, * method.getReturnType(), imports)); * bean.set(change.getPropertyName(), * newValue); } catch (final Throwable t) { * LOG.warn( * "Could not deserialize property: " + * change.getPropertyName(), t); } */ } }); } } this.config = config; // TODO use event listeners of Mutable interface to dynamically add // Converters at runtime } @SuppressWarnings("rawtypes") @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { // LOG.trace("Calling " + this.type.getSimpleName() + "#" // + method.getName() + "()"); final String beanProp = method.getName(); switch (args == null ? 0 : args.length) { case 0: if (beanProp.equals("toString")) { if (Wrapper.class.isAssignableFrom(this.type)) return this.bean.get("wrap"); JsonUtil.checkRegistered(JsonUtil.getJOM(), this.type); return this.bean.toString(); } if (beanProp.equals("hashCode")) return this.bean.hashCode(); // ! can't intercept call to native method // if (method.getName().equals("getClass")) // return this.type; if (!method.getReturnType().equals(Void.TYPE)) { if (method.isDefault()) return ReflectUtil.invokeDefaultMethod(proxy, method, args); Object result = this.bean.any().get(beanProp); if (result == null) { if (this.config != null) { result = method.invoke(this.config, args); // LOG.trace( // "Property {}#{}() == {} [current Config]", // this.type.getSimpleName(), beanProp, result); } // else // LOG.trace("Property {}#{}() == null [no config]", // this.type.getSimpleName(), beanProp); } return result; } break; case 1: if (beanProp.equals("equals")) return this.bean.equals(args[0]); final DynaBean.BeanProxy comparable = this.type.getAnnotation(DynaBean.BeanProxy.class); if (beanProp.equals("compareTo") && comparable != null) return DynaBean.getComparator(comparable).compare((Comparable) this.bean, (Comparable) args[0]); if (method.getParameterTypes().length == 1 && method.getParameterTypes()[0].isAssignableFrom(args[0].getClass()) //&& method.getName().startsWith( "set" ) ) //&& method.getReturnType().equals( Void.TYPE ) ) { this.bean.set(beanProp, args[0]); return null; // setters return void } break; } if (this.config != null) { // LOG.trace("Passing call to Config"); return method.invoke(this.config, args); } if (method.getReturnType().equals(Void.TYPE)) { LOG.warn("Ignoring call to: void " + this.type.getSimpleName() + "#" + beanProp + "(" + Arrays.asList(args) + ")"); return null; } throw ExceptionFactory.createUnchecked("{} ({}) value not set: {}#{}({})", DynaBean.class.getSimpleName(), method.getReturnType().isPrimitive() ? "primitive" : "Object", this.type.getSimpleName(), beanProp, Arrays.asList(args)); } } /** * @param <T> * @param wrapperType * @return */ static final <T> JsonSerializer<T> createJsonSerializer(final Class<T> type) { return new JsonSerializer<T>() { @Override public void serialize(final T value, final JsonGenerator jgen, final SerializerProvider serializers) throws IOException, JsonProcessingException { // non-Proxy objects get default treatment if (!Proxy.isProxyClass(value.getClass())) { @SuppressWarnings("unchecked") final JsonSerializer<T> ser = (JsonSerializer<T>) serializers .findValueSerializer(value.getClass()); if (ser != this) ser.serialize(value, jgen, serializers); else LOG.warn("Problem serializing: {}", value); return; } // BeanWrapper gets special treatment if (DynaBeanInvocationHandler.class.isInstance(Proxy.getInvocationHandler(value))) { final DynaBeanInvocationHandler handler = (DynaBeanInvocationHandler) Proxy .getInvocationHandler(value); // Wrapper extensions get special treatment if (Wrapper.class.isAssignableFrom(handler.type)) { final Object wrap = handler.bean.get("wrap"); serializers.findValueSerializer(wrap.getClass(), null).serialize(wrap, jgen, serializers); return; } // Config (Accessible) extensions get special treatment else if (Accessible.class.isAssignableFrom(handler.type)) { final Map<String, Object> copy = new HashMap<>(handler.bean.any()); final Accessible config = (Accessible) handler.config; for (String key : config.propertyNames()) copy.put(key, config.getProperty(key)); serializers.findValueSerializer(copy.getClass(), null).serialize(copy, jgen, serializers); return; } else if (Config.class.isAssignableFrom(handler.type)) throw new JsonGenerationException("BeanWrapper should extend " + Accessible.class.getName() + " required for serialization: " + Arrays.asList(handler.type.getInterfaces()), jgen); // BeanWrappers that do not extend OWNER API's Config serializers.findValueSerializer(handler.bean.getClass(), null).serialize(handler.bean, jgen, serializers); return; } // Config (Accessible) gets special treatment if (Accessible.class.isInstance(value)) { final Accessible config = (Accessible) value; final Properties entries = new Properties(); for (String key : config.propertyNames()) entries.put(key, config.getProperty(key)); serializers.findValueSerializer(entries.getClass(), null).serialize(entries, jgen, serializers); return; } if (Config.class.isInstance(value)) throw new JsonGenerationException("Config should extend " + Accessible.class.getName() + " required for serialization: " + Arrays.asList(value.getClass().getInterfaces()), jgen); throw new JsonGenerationException( "No serializer found for proxy of: " + Arrays.asList(value.getClass().getInterfaces()), jgen); } }; } /** * @param referenceType * @param <S> * @param <T> * @return */ static final <S, T> JsonDeserializer<T> createJsonDeserializer(final ObjectMapper om, final Class<T> resultType, final Properties... imports) { return new JsonDeserializer<T>() { @Override public T deserializeWithType(final JsonParser jp, final DeserializationContext ctxt, final TypeDeserializer typeDeserializer) throws IOException, JsonProcessingException { return deserialize(jp, ctxt); } @Override public T deserialize(final JsonParser jp, final DeserializationContext ctxt) throws IOException, JsonProcessingException { if (jp.getCurrentToken() == JsonToken.VALUE_NULL) return null; // if( Wrapper.class.isAssignableFrom( resultType ) ) // { // // FIXME // LOG.trace( "deser wrapper intf of {}", jp.getText() ); // return (T) Wrapper.Util.valueOf( jp.getText(), // resultType.asSubclass( Wrapper.class ) ); // } if (Config.class.isAssignableFrom(resultType)) { final Map<String, Object> entries = jp.readValueAs(new TypeReference<Map<String, Object>>() { }); final Iterator<Entry<String, Object>> it = entries.entrySet().iterator(); for (Entry<String, Object> next = null; it.hasNext(); next = it.next()) if (next != null && next.getValue() == null) { LOG.trace("Ignoring null value: {}", next); it.remove(); } return resultType.cast(ConfigFactory.create(resultType.asSubclass(Config.class), entries)); } // else if (Config.class.isAssignableFrom(resultType)) // throw new JsonGenerationException( // "Config does not extend "+Mutable.class.getName()+" required for deserialization: " // + Arrays.asList(resultType // .getInterfaces())); // can't parse directly to interface type final DynaBean bean = new DynaBean(); final TreeNode tree = jp.readValueAsTree(); // override attributes as defined in interface getters final Set<String> attributes = new HashSet<>(); for (Method method : resultType.getMethods()) { if (method.getReturnType().equals(Void.TYPE) || method.getParameterTypes().length != 0) continue; final String attribute = method.getName(); if (attribute.equals("toString") || attribute.equals("hashCode")) continue; attributes.add(attribute); final TreeNode value = tree.get(attribute);// bean.any().get(attributeName); if (value == null) continue; bean.set(method.getName(), om.treeToValue(value, JsonUtil.checkRegistered(om, method.getReturnType(), imports))); } if (tree.isObject()) { // keep superfluous properties as TreeNodes, just in case final Iterator<String> fieldNames = tree.fieldNames(); while (fieldNames.hasNext()) { final String fieldName = fieldNames.next(); if (!attributes.contains(fieldName)) bean.set(fieldName, tree.get(fieldName)); } } else if (tree.isValueNode()) { for (Class<?> type : resultType.getInterfaces()) for (Method method : type.getDeclaredMethods()) { // LOG.trace( "Scanning {}", method ); if (method.isAnnotationPresent(JsonProperty.class)) { final String property = method.getAnnotation(JsonProperty.class).value(); // LOG.trace( "Setting {}: {}", property, // ((ValueNode) tree).textValue() ); bean.set(property, ((ValueNode) tree).textValue()); } } } else throw ExceptionFactory.createUnchecked("Expected {} but parsed: {}", resultType, tree.getClass()); return DynaBean.proxyOf(om, resultType, bean, imports); } }; } /** */ public static <T> void registerType(final ObjectMapper om, final Class<T> type, final Properties... imports) { // TODO implement dynamic generic Converter(s) for JSON bean // properties ? // if (Config.class.isAssignableFrom(type)) // { // final Class<?> editorType = new // JsonPropertyEditor<T>().getClass(); // PropertyEditorManager.registerEditor(type, editorType); // LOG.trace("Registered " + editorType + " - " // + PropertyEditorManager.findEditor(type)); // } om.registerModule(new SimpleModule().addSerializer(type, createJsonSerializer(type)).addDeserializer(type, createJsonDeserializer(om, type, imports))); } /** */ private static final Map<BeanProxy, Comparator<?>> COMPARATOR_CACHE = new TreeMap<>(); /** * @param annot the {@link BeanProxy} instance for the type of wrapper of * {@link DynaBean}s containing the {@link Comparable} value type * in the annotated property key * @return a (cached) comparator */ @SuppressWarnings({ "unchecked", "rawtypes" }) public static <S extends Comparable> Comparator<S> getComparator(final BeanProxy annot) { if (annot.comparableOn().isEmpty()) return null; synchronized (COMPARATOR_CACHE) { Comparator<S> result = (Comparator<S>) COMPARATOR_CACHE.get(annot); if (result == null) { result = new Comparator<S>() { @Override public int compare(final S o1, final S o2) { final S key1 = (S) ((DynaBeanInvocationHandler) Proxy.getInvocationHandler(o1)).bean.any() .get(annot.comparableOn()); final S key2 = (S) ((DynaBeanInvocationHandler) Proxy.getInvocationHandler(o2)).bean.any() .get(annot.comparableOn()); return key1.compareTo(key2); } }; LOG.trace("Created comparator for " + annot); COMPARATOR_CACHE.put(annot, result); } return result; } } /** * @param type the type of {@link Proxy} to generate * @param imports default value {@link Properties} of the bean * @return a {@link Proxy} instance backed by an empty {@link DynaBean} */ public static <T> T proxyOf(final Class<T> type, final Properties... imports) { return proxyOf(type, new DynaBean(), imports); } /** * @param type the type of {@link Proxy} to generate * @param bean the (prepared) {@link DynaBean} for proxied getters/setters * @param imports default value {@link Properties} of the bean * @return a {@link Proxy} instance backed by an empty {@link DynaBean} */ public static <T> T proxyOf(final Class<T> type, final DynaBean bean, final Properties... imports) { return proxyOf(JsonUtil.getJOM(), type, bean, imports); } /** * @param om the {@link ObjectMapper} for get and set de/serialization * @param type the type of {@link Proxy} to generate * @param bean the (prepared) {@link DynaBean} for proxied getters/setters * @param imports default value {@link Properties} of the bean * @return a {@link Proxy} instance backed by an empty {@link DynaBean} */ @SuppressWarnings("unchecked") public static <T> T proxyOf(final ObjectMapper om, final Class<T> type, final DynaBean bean, final Properties... imports) { // if( !type.isAnnotationPresent( BeanProxy.class ) ) // throw ExceptionFactory.createUnchecked( "{} is not a @{}", type, // BeanProxy.class.getSimpleName() ); return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, new DynaBeanInvocationHandler(om, type, bean, imports)); } /** * {@link ProxyProvider} * * @param <T> * @version $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ * @author Rick van Krevelen */ public static class ProxyProvider<T> implements Provider<T> { /** cache of type arguments for known {@link Proxy} sub-types */ private static final Map<Class<?>, List<Class<?>>> PROXY_TYPE_ARGUMENT_CACHE = new HashMap<>(); /** * @param proxyType should be a non-abstract concrete {@link Class} that * has a public zero-arg constructor * @return the new {@link ProxyProvider} instance */ public static <T> ProxyProvider<T> of(final Class<T> proxyType, final Properties... imports) { return of(JsonUtil.getJOM(), proxyType, imports); } /** * @param om the {@link ObjectMapper} for get and set de/serialization * @param proxyType should be a non-abstract concrete {@link Class} that * has a public zero-arg constructor * @return the new {@link ProxyProvider} instance */ public static <T> ProxyProvider<T> of(final ObjectMapper om, final Class<T> proxyType, final Properties... imports) { return new ProxyProvider<T>(om, proxyType, new DynaBean(), imports); } /** * @param om the {@link ObjectMapper} for get and set de/serialization * @param beanType should be a non-abstract concrete {@link Class} that * has a public zero-arg constructor * @param cache the {@link Map} of previously created instances * @return the cached (new) {@link ProxyProvider} instance */ public static <T> ProxyProvider<T> of(final ObjectMapper om, final Class<T> beanType, final Map<Class<?>, ProxyProvider<?>> cache, final Properties... imports) { if (cache == null) return of(om, beanType, imports); synchronized (cache) { @SuppressWarnings("unchecked") ProxyProvider<T> result = (ProxyProvider<T>) cache.get(beanType); if (result == null) { result = of(om, beanType, imports); cache.put(beanType, result); } return result; } } /** */ private final ObjectMapper om; /** */ private final Class<T> proxyType; /** */ private final DynaBean bean; /** */ private final Properties[] imports; /** * {@link ProxyProvider} constructor * * @param om * @param proxyType * @param bean the (possibly prepared) {@link DynaBean} * @param imports */ public ProxyProvider(final ObjectMapper om, final Class<T> proxyType, final DynaBean bean, final Properties... imports) { this.om = om; this.proxyType = proxyType; this.bean = bean; this.imports = imports; } @Override public T get() { try { @SuppressWarnings("unchecked") final Class<T> proxyType = this.proxyType == null ? (Class<T>) TypeArguments.of(ProxyProvider.class, getClass(), PROXY_TYPE_ARGUMENT_CACHE) .get(0) : this.proxyType; return DynaBean.proxyOf(this.om, proxyType, this.bean, this.imports); } catch (final Throwable e) { Thrower.rethrowUnchecked(e); return null; } } } // /** // * {@link Builder} // * // * @param <T> the result type // * @param <THIS> the builder type // * @version $Id: a29f270577feb1bdcaea1b2d76fcd63edeace209 $ // * @author Rick van Krevelen // */ // public static class Builder<T, THIS extends Builder<T, THIS>> // extends ProxyProvider<T> // { // // /** */ // private final DynaBean bean; // // /** // * {@link Builder} constructor, to be extended by a public zero-arg // * constructor in concrete sub-types // */ // protected Builder( final Properties... imports ) // { // this( JsonUtil.getJOM(), new DynaBean(), imports ); // } // // /** // * {@link Builder} constructor, to be extended by a public zero-arg // * constructor in concrete sub-types // */ // protected Builder( final ObjectMapper om, final Properties... imports ) // { // this( om, new DynaBean(), imports ); // } // // /** // * {@link Builder} constructor, to be extended by a public zero-arg // * constructor in concrete sub-types // */ // protected Builder( final ObjectMapper om, final DynaBean bean, // final Properties... imports ) // { // super( om, null, bean, imports ); // this.bean = bean; // } // // /** // * helper-method // * // * @param key // * @param returnType // * @return the currently set value, or {@code null} if not set // */ // protected <S> S get( final String key, final Class<S> returnType ) // { // return returnType.cast( this.bean.get( key ) ); // } // // /** // * @param key // * @param value // * @return // */ // @SuppressWarnings( "unchecked" ) // public THIS with( final String key, final Object value ) // { // this.bean.set( key, value ); // return (THIS) this; // } // // public THIS with( final String key, final TreeNode value, // final Class<?> valueType ) // { // return (THIS) with( key, JsonUtil.valueOf( value, valueType ) ); // } // // /** // * @return this Builder with the immutable bean // */ // @SuppressWarnings( "unchecked" ) // public THIS lock() // { // this.bean.lock(); // return (THIS) this; // } // // /** // * @return the provided instance of <T> // */ // public T build() // { // return get(); // } // } }