
com.eventsourcing.layout.Layout Maven / Gradle / Ivy
Show all versions of eventsourcing-layout Show documentation
/**
* 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.
*
* Layout name, property names and property types are used to deterministically calculate hash (used for versioning).
*
*
Property qualification
*
* Only certain properties will be included into the layout. Here's the definitive list of criteria:
*
*
* - Has a getter (fluent or JavaBean style)
* - Has a matching parameter in the constructor (same parameter name or same name through {@link PropertyName}
* parameter annotation)
* - Must be of a supported type (see {@link TypeHandler#lookup(ResolvedType)})
*
*
* Additionally, inherited properties will be qualified by the following criteria:
*
* - Has both a getter and a setter (fluent or JavaBean style)
* - Has a matching parameter in the any parent's class constructor (same parameter name or same name through
* {@link PropertyName} parameter annotation)
* - Must be of a supported type (see {@link TypeHandler#lookup(ResolvedType)})
*
*
*
* @param 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 {
public static final String DIGEST_ALGORITHM = "SHA-1";
/**
* Qualified properties. See {@link Layout} for definition
*/
@Getter
private final List> properties;
@Getter
private final List> 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 constructor;
@Getter
private Class layoutClass;
private TypeResolver typeResolver;
private MethodHandles.Lookup methodHandles;
private Map setters = new HashMap<>();
@LayoutConstructor
public Layout(String name, List> 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 Layout forClass(Class klass)
throws TypeHandler.TypeHandlerException, IntrospectionException, NoSuchAlgorithmException,
IllegalAccessException {
return new Layout<>(klass);
}
private Layout(Class klass)
throws IntrospectionException, NoSuchAlgorithmException, IllegalAccessException,
TypeHandler.TypeHandlerException {
typeResolver = new TypeResolver();
methodHandles = MethodHandles.lookup();
layoutClass = klass;
properties = new ArrayList<>();
constructorProperties = new ArrayList<>();
ClassAnalyzer.Constructor 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 property : properties) {
digest.update(property.getName().getBytes());
digest.update(property.getTypeHandler().getFingerprint());
}
this.hash = digest.digest();
}
@SneakyThrows
private ClassAnalyzer.Constructor findLayoutConstructor(Class 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 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 fluent = retrieveGetter(name, parameter.getType());
Optional getX = retrieveGetter("get" + capitalizedName, parameter.getType());
Optional isX = (parameter.getType() == Boolean.TYPE || parameter.getType() == Boolean.class)
? retrieveGetter("is" + capitalizedName, parameter.getType()) : Optional.empty();
Optional fluentSetter = retrieveSetter(name, parameter.getType());
Optional setX = retrieveSetter("set" + capitalizedName, parameter.getType());
// prefer in this order: getX, isX, fluent
Optional> getter = Stream.of(getX, isX, fluent).filter(Optional::isPresent).findFirst();
if (!getter.isPresent()) {
throw new IllegalArgumentException("No getter found for " + 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 property = new Property<>(name, resolvedType,
TypeHandler.lookup(resolvedType),
new GetterFunction(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 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 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 getProperty(String name) throws NoSuchElementException {
Property property = getNullableProperty(name);
if (property != null) return property;
throw new NoSuchElementException();
}
private Property getNullableProperty(String name) {
for (Property 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, Object> properties)
throws Throwable {
Object[] args = new Object[constructor.getParameterCount()];
BinarySerialization serialization = BinarySerialization.getInstance();
for (int i = 0; i < args.length; i++) {
Property property = this.constructorProperties.get(i);
Optional