All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.eventsourcing.layout.Layout Maven / Gradle / Ivy

There is a newer version: 0.4.6
Show newest version
/**
 * 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 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 entry : setters.entrySet()) { Optional 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 findProperty(Map, Object> properties, String name) { for (Map.Entry, 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 property : properties) { builder.append(" ").append(property.toString()).append("\n"); } return builder.toString(); } private static class GetterFunction implements Function { private final MethodHandle getterHandler; public GetterFunction(MethodHandle getterHandler) {this.getterHandler = getterHandler;} @Override @SneakyThrows public Object apply(T t) { return getterHandler.invoke(t); } } }