/**
* Copyright (c) 2016, All Contributors (see CONTRIBUTORS file)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.eventsourcing.layout;
import com.eventsourcing.layout.binary.BinarySerialization;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.io.BaseEncoding;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.unprotocols.coss.RFC;
import org.unprotocols.coss.Raw;
import java.beans.IntrospectionException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.*;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Layout is a snapshot of a class for the purpose of versioning, serialization and deserialization.
* <p>
* Layout name, property names and property types are used to deterministically calculate hash (used for versioning).
* <p>
* <h1>Property qualification</h1>
* <p>
* Only certain properties will be included into the layout. Here's the definitive list of criteria:
* <p>
* <ul>
* <li>Has a getter (fluent or JavaBean style)</li>
* <li>Has a matching parameter in the constructor (same parameter name or same name through {@link PropertyName}
* parameter annotation)</li>
* <li>Must be of a supported type (see {@link TypeHandler#lookup(ResolvedType)})</li>
* </ul>
*
* Additionally, inherited properties will be qualified by the following criteria:
* <ul>
* <li>Has both a getter and a setter (fluent or JavaBean style)</li>
* <li>Has a matching parameter in the any parent's class constructor (same parameter name or same name through
* {@link PropertyName} parameter annotation)</li>
* <li>Must be of a supported type (see {@link TypeHandler#lookup(ResolvedType)})</li>
* </ul>
* <p>
*
* @param <T> Bean's class
*/
@LayoutName("rfc.eventsourcing.com/spec:7/LDL/#Layout")
@Raw @RFC(url = "http://rfc.eventsourcing.com/spec:7/LDL/", revision = "Jun 18, 2016")
@Slf4j
public class Layout<T> {
public static final String DIGEST_ALGORITHM = "SHA-1";
/**
* Qualified properties. See {@link Layout} for definition
*/
@Getter
private final List<Property<T>> properties;
@Getter
private final List<Property<T>> constructorProperties;
/**
* Layout name (derived from class or overriden with {@link LayoutName})
*/
@Getter
private final String name;
/**
* Layout's hash (fingerprint)
*/
@Getter
private byte[] hash;
@Getter
private Constructor<T> constructor;
@Getter
private Class<T> layoutClass;
private TypeResolver typeResolver;
private MethodHandles.Lookup methodHandles;
private Map<String, MethodHandle> setters = new HashMap<>();
@LayoutConstructor
public Layout(String name, List<Property<T>> properties) {
this.name = name;
this.properties = properties;
this.constructorProperties = new ArrayList<>();
}
/**
* Creates a Layout for a class. The class MUST define a constructor with properties. If multiple public
* constructors are defined, one must be chosen with {@link LayoutConstructor}. Otherwise, by default,
* a preference is given to the widest constructor (the one with most parameters).
*
* @param klass Type
* @throws IntrospectionException
* @throws NoSuchAlgorithmException
* @throws IllegalAccessException
* @throws com.eventsourcing.layout.TypeHandler.TypeHandlerException
*/
public static <T> Layout<T> forClass(Class<T> klass)
throws TypeHandler.TypeHandlerException, IntrospectionException, NoSuchAlgorithmException,
IllegalAccessException {
return new Layout<>(klass);
}
private Layout(Class<T> klass)
throws IntrospectionException, NoSuchAlgorithmException, IllegalAccessException,
TypeHandler.TypeHandlerException {
typeResolver = new TypeResolver();
methodHandles = MethodHandles.lookup();
layoutClass = klass;
properties = new ArrayList<>();
constructorProperties = new ArrayList<>();
ClassAnalyzer.Constructor<T> analyzerConstructor = findLayoutConstructor(layoutClass);
constructor = analyzerConstructor.getConstructor();
deriveProperties(layoutClass, analyzerConstructor, false);
// Prepare the hash
MessageDigest digest = MessageDigest.getInstance(DIGEST_ALGORITHM);
name = klass.isAnnotationPresent(LayoutName.class) ? klass.getAnnotation(LayoutName.class)
.value() : klass.getName();
// It is important to include class name into the hash as there could be situations
// when POJOs have indistinguishable layouts, and therefore it is impossible to
// guarantee that we'd pick the right class
digest.update(name.getBytes());
for (Property<T> property : properties) {
digest.update(property.getName().getBytes());
digest.update(property.getTypeHandler().getFingerprint());
}
this.hash = digest.digest();
}
@SneakyThrows
private <X> ClassAnalyzer.Constructor findLayoutConstructor(Class<X> klass) {
ClassAnalyzer analyzer = new JavaClassAnalyzer();
if (klass.isAnnotationPresent(UseClassAnalyzer.class)) {
analyzer = klass.getAnnotation(UseClassAnalyzer.class).value().newInstance();
}
ClassAnalyzer.Constructor[] constructors = analyzer.getConstructors(klass);
// Must have at least one public constructor
if (constructors.length == 0) {
throw new IllegalArgumentException(klass + " doesn't have any public constructors");
}
// Prefer wider constructors
List<ClassAnalyzer.Constructor> constructorList = Arrays.asList(constructors);
constructorList.sort((o1, o2) -> Integer.compare(o2.getParameters().length, o1.getParameters().length));
// Pick the first constructor by default (if there will be only one)
ClassAnalyzer.Constructor constructor = constructorList.get(0);
boolean ambiguityDetected = false;
for (ClassAnalyzer.Constructor c : constructorList) {
// If annotated as a layout constructor, pick it, end of story
if (c.isLayoutConstructor()) {
return c;
}
// If a non-annotated constructor of the same width is found,
// when there's no annotated constructor, it might cause an
// ambiguity
if (c != constructor && c.getParameters().length == constructor.getParameters().length) {
ambiguityDetected = true;
}
}
if (ambiguityDetected) {
throw new IllegalArgumentException(klass + " has more than one constructor with " +
constructor.getParameters().length +
" parameters and no @LayoutConstructor-annotated constructor");
}
return constructor;
}
private void deriveProperties(Class<?> klass, ClassAnalyzer.Constructor constructor, boolean parentClass)
throws TypeHandler.TypeHandlerException,
IllegalAccessException {
ClassAnalyzer.Parameter[] parameters = constructor.getParameters();
// Require parameter names
for (ClassAnalyzer.Parameter parameter : parameters) {
String name = parameter.getName();
// if there's such property already, skip processing the parameter
if (getNullableProperty(name) != null) {
continue;
}
String capitalizedName = capitalizeFirstLetter(name);
// discover a getter
Optional<Method> fluent = retrieveGetter(name, parameter.getType());
Optional<Method> getX = retrieveGetter("get" + capitalizedName, parameter.getType());
Optional<Method> isX = (parameter.getType() == Boolean.TYPE || parameter.getType() == Boolean.class)
? retrieveGetter("is" + capitalizedName, parameter.getType()) : Optional.empty();
Optional<Method> fluentSetter = retrieveSetter(name, parameter.getType());
Optional<Method> setX = retrieveSetter("set" + capitalizedName, parameter.getType());
// prefer in this order: getX, isX, fluent
Optional<Optional<Method>> getter = Stream.of(getX, isX, fluent).filter(Optional::isPresent).findFirst();
if (!getter.isPresent()) {
throw new IllegalArgumentException("No getter found for " + parameter.getType() +
" " + layoutClass.getName() + "." + name + ". ");
}
// Not a valid property if it doesn't have a setter and a setter is required
if (parentClass && !setX.isPresent() && !fluentSetter.isPresent()) {
continue;
}
if (parentClass) {
Method setterMethod = Stream.of(setX, fluentSetter).filter(Optional::isPresent).findFirst().get().get();
MethodHandle setterHandler = methodHandles.unreflect(setterMethod);
setters.put(name, setterHandler);
}
Method method = getter.get().get();
ResolvedType resolvedType = typeResolver.resolve(method.getGenericReturnType());
MethodHandle getterHandler = methodHandles.unreflect(method);
Property<T> property = new Property<>(name, resolvedType,
TypeHandler.lookup(resolvedType),
new GetterFunction<T>(getterHandler));
properties.add(property);
if (!parentClass) {
constructorProperties.add(property);
}
}
Class superclass = klass.getSuperclass();
if (superclass != Object.class) {
ClassAnalyzer.Constructor parentConstructor = findLayoutConstructor(superclass);
deriveProperties(superclass, parentConstructor, true);
}
// Sort properties lexicographically (by default, they seem to be sorted anyway,
// however, no such guarantee was found in the documentation upon brief inspection)
properties.sort((x, y) -> x.getName().compareTo(y.getName()));
}
private Optional<Method> retrieveGetter(String name, Class<?> type) {
try {
Method method = layoutClass.getMethod(name);
if (Modifier.isPublic(method.getModifiers()) &&
method.getReturnType() == type) {
return Optional.of(method);
} else {
return Optional.empty();
}
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}
private Optional<Method> retrieveSetter(String name, Class<?> type) {
try {
Method method = layoutClass.getMethod(name, type);
if (Modifier.isPublic(method.getModifiers())) {
return Optional.of(method);
} else {
return Optional.empty();
}
} catch (NoSuchMethodException e) {
return Optional.empty();
}
}
private String capitalizeFirstLetter(String input) {
return input.substring(0, 1).toUpperCase() + input.substring(1);
}
/**
* Get a property by name
* @param name property name
* @return
* @throws NoSuchElementException if no such property is defined
*/
public Property<T> getProperty(String name) throws NoSuchElementException {
Property<T> property = getNullableProperty(name);
if (property != null) return property;
throw new NoSuchElementException();
}
private Property<T> getNullableProperty(String name) {
for (Property<T> property : properties) {
if (property.getName().contentEquals(name)) {
return property;
}
}
return null;
}
/**
* Instantiate the layout class with default properties
* @return
* @throws IllegalAccessException
* @throws InstantiationException
* @throws InvocationTargetException
*/
public T instantiate() throws Throwable {
return instantiate(new HashMap<>());
}
/**
* Instantiate the layout class with fully or partially supplied property values
* @param properties property values
* @return
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws InstantiationException
*/
public T instantiate(Map<Property<T>, Object> properties)
throws Throwable {
Object[] args = new Object[constructor.getParameterCount()];
BinarySerialization serialization = BinarySerialization.getInstance();
for (int i = 0; i < args.length; i++) {
Property<T> property = this.constructorProperties.get(i);
Optional<Object> suppliedProperty = findProperty(properties, property.getName());
if (suppliedProperty.isPresent()) {
args[i] = suppliedProperty.get();
} else {
TypeHandler typeHandler = property.getTypeHandler();
ByteBuffer buffer = serialization.getSerializer(typeHandler).serialize(typeHandler, args[i]);
buffer.rewind();
Object o = serialization.getDeserializer(typeHandler).deserialize(typeHandler, buffer);
args[i] = o;
}
Class<?> constructorArgType = constructor.getParameterTypes()[i];
if (!isAssignableFrom(constructorArgType, args[i].getClass())) {
throw new IllegalArgumentException("Property " + property.getName() + ": expected " +
constructorArgType + ", got " + args[i].getClass());
}
}
T t = constructor.newInstance(args);
if (!setters.isEmpty()) {
for (Map.Entry<String, MethodHandle> entry : setters.entrySet()) {
Optional<Object> suppliedProperty = findProperty(properties, entry.getKey());
if (suppliedProperty.isPresent()) {
entry.getValue().invoke(t, suppliedProperty.get());
}
}
}
return t;
}
private boolean isAssignableFrom(Class<?> base, Class<?> klass) {
return toNonPrimitiveClass(base).isAssignableFrom(toNonPrimitiveClass(klass));
}
private Class<?> toNonPrimitiveClass(Class<?> klass) {
if (klass.equals(Byte.TYPE)) {
return Byte.class;
}
if (klass.equals(Short.TYPE)) {
return Short.class;
}
if (klass.equals(Integer.TYPE)) {
return Integer.class;
}
if (klass.equals(Long.TYPE)) {
return Long.class;
}
if (klass.equals(Boolean.TYPE)) {
return Boolean.class;
}
if (klass.equals(Float.TYPE)) {
return Float.class;
}
if (klass.equals(Double.TYPE)) {
return Double.class;
}
return klass;
}
private Optional<Object> findProperty(Map<Property<T>, Object> properties, String name) {
for (Map.Entry<Property<T>, Object> entry : properties.entrySet()) {
if (entry.getKey().getName().contentEquals(name)) {
return Optional.ofNullable(entry.getValue());
}
}
return Optional.empty();
}
@Override
public boolean equals(Object obj) {
return obj instanceof Layout && Arrays.equals(getHash(), ((Layout) obj).getHash());
}
public String toString() {
StringBuilder builder = new StringBuilder().append(
layoutClass.getName() + " " + BaseEncoding.base16().encode(hash))
.append("\n");
for (Property<T> property : properties) {
builder.append(" ").append(property.toString()).append("\n");
}
return builder.toString();
}
private static class GetterFunction<T> implements Function<T, Object> {
private final MethodHandle getterHandler;
public GetterFunction(MethodHandle getterHandler) {this.getterHandler = getterHandler;}
@Override
@SneakyThrows
public Object apply(T t) {
return getterHandler.invoke(t);
}
}
}