package de.cronn.reflection.util.immutable; import static net.bytebuddy.matcher.ElementMatchers.*; import java.io.File; import java.lang.annotation.Annotation; import java.net.URI; import java.nio.file.Path; import java.time.temporal.Temporal; import java.time.temporal.TemporalAmount; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import org.objenesis.ObjenesisHelper; import de.cronn.reflection.util.ClassUtils; import de.cronn.reflection.util.PropertyUtils; import de.cronn.reflection.util.immutable.collection.DeepImmutableCollection; import de.cronn.reflection.util.immutable.collection.DeepImmutableList; import de.cronn.reflection.util.immutable.collection.DeepImmutableMap; import de.cronn.reflection.util.immutable.collection.DeepImmutableSet; import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.method.MethodDescription.SignatureToken; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.implementation.ExceptionMethod; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatcher.Junction; import net.bytebuddy.matcher.ElementMatchers; public final class ImmutableProxy { static final String DELEGATE_FIELD_NAME = "$delegate"; private static final Map<Class<?>, Class<?>> immutableProxyClassCache = new ConcurrentHashMap<>(); private ImmutableProxy() { } public static <T> T create(T instance) { if (isImmutable(instance)) { return instance; } Class<? extends T> proxyClass = getOrCreateProxyClass(instance); T proxy = ObjenesisHelper.newInstance(proxyClass); PropertyUtils.writeDirectly(proxy, DELEGATE_FIELD_NAME, instance); return proxy; } public static <T> Collection<T> create(Collection<T> collection) { return new DeepImmutableCollection<>(collection); } public static <T> List<T> create(List<T> list) { return new DeepImmutableList<>(list); } public static <T> Set<T> create(Set<T> set) { return new DeepImmutableSet<>(set); } public static <K, V> Map<K, V> create(Map<K, V> map) { return new DeepImmutableMap<>(map); } public static <T> T unwrap(T immutableProxy) { if (!isImmutableProxy(immutableProxy)) { return immutableProxy; } else { return PropertyUtils.readDirectly(immutableProxy, DELEGATE_FIELD_NAME); } } static boolean isImmutable(Object value) { if (value == null) { return true; } else if (isImmutableProxy(value)) { return true; } else if (value instanceof String) { return true; } else if (value instanceof Number) { return true; } else if (value instanceof Boolean) { return true; } else if (value instanceof Character) { return true; } else if (value instanceof Temporal) { return true; } else if (value instanceof TemporalAmount) { return true; } else if (value instanceof UUID) { return true; } else if (value instanceof File) { return true; } else if (value instanceof Path) { return true; } else if (value instanceof URI) { return true; } else if (isEnumValue(value)) { return true; } else { return false; } } private static boolean isEnumValue(Object value) { return value.getClass().isEnum() || (value.getClass().getSuperclass() != null && value.getClass().getSuperclass().isEnum()); } @SuppressWarnings("unchecked") private static <T> Class<? extends T> getOrCreateProxyClass(T instance) { Class<T> realClass = ClassUtils.getRealClass(instance); return (Class<? extends T>) immutableProxyClassCache.computeIfAbsent(realClass, clazz -> createProxyClass((Class<T>) clazz)); } private static <T> Class<? extends T> createProxyClass(Class<T> clazz) { return new ByteBuddy() .subclass(clazz) .implement(Immutable.class) .defineField(DELEGATE_FIELD_NAME, clazz) .method(any()) .intercept(ExceptionMethod.throwing(UnsupportedOperationException.class, "This instance is immutable." + " Annotate the method with @" + ReadOnly.class.getSimpleName() + " if this is a false-positive.")) .method(isReadyOnlyMethod()) .intercept(MethodDelegation.to(GenericImmutableProxyForwarder.class)) .method(isReadyOnlyMethod().and(returns(Long.class).or(returns(long.class)))) .intercept(MethodDelegation.to(ImmutableProxyForwarderLong.class)) .method(isReadyOnlyMethod().and(returns(Integer.class).or(returns(int.class)))) .intercept(MethodDelegation.to(ImmutableProxyForwarderInteger.class)) .method(isReadyOnlyMethod().and(returns(Boolean.class).or(returns(boolean.class)))) .intercept(MethodDelegation.to(ImmutableProxyForwarderBoolean.class)) .method(isReadyOnlyMethod().and(returns(String.class))) .intercept(MethodDelegation.to(ImmutableProxyForwarderString.class)) .make() .load(ImmutableProxy.class.getClassLoader()) .getLoaded(); } private static Junction<MethodDescription> isReadyOnlyMethod() { return not(isSetter()) .and(isGetter() .or(isHashCode()).or(isEquals()).or(isToString()).or(isClone()) .or(isDeclaredBy(Object.class)) .or(isAnnotatedWith(ReadOnly.class))); } private static ElementMatcher<MethodDescription> isAnnotatedWith(Class<? extends Annotation> annotation) { return target -> { TypeDefinition type = target.getDeclaringType(); SignatureToken methodSignature = target.asSignatureToken(); return isAnnotatedWith(methodSignature, type, annotation); }; } private static boolean isAnnotatedWith(SignatureToken methodSignature, TypeDefinition type, Class<? extends Annotation> annotation) { if (type == null || type.equals(TypeDescription.OBJECT)) { return false; } if (hasMethodAnnotatedWith(methodSignature, type, annotation)) { return true; } Iterable<? extends TypeDefinition> interfaces = type.getInterfaces(); for (TypeDefinition interfaceType : interfaces) { if (hasMethodAnnotatedWith(methodSignature, interfaceType, annotation)) { return true; } for (TypeDescription.Generic interfaceSuperclass : interfaceType.getInterfaces()) { if (isAnnotatedWith(methodSignature, interfaceSuperclass, annotation)) { return true; } } } return isAnnotatedWith(methodSignature, type.getSuperClass(), annotation); } private static boolean hasMethodAnnotatedWith(SignatureToken methodSignature, TypeDefinition type, Class<? extends Annotation> annotation) { return !type.getDeclaredMethods() .filter(hasMethodName(methodSignature.getName()) .and(takesArguments(methodSignature.getParameterTypes())) .and(ElementMatchers.isAnnotatedWith(annotation))) .isEmpty(); } public static boolean isImmutableProxy(Object object) { if (object == null) { return false; } return isImmutableProxyClass(object.getClass()); } public static boolean isImmutableProxyClass(Class<?> beanClass) { return Immutable.class.isAssignableFrom(beanClass); } static void clearCache() { immutableProxyClassCache.clear(); } }