package de.axone.equals;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Currency;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.persistence.Id;
import javax.persistence.Transient;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.axone.equals.EqualsClass.Select;
import de.axone.equals.SynchroMapper.DefaultSynchroMapper;
import de.axone.exception.Assert;
/**
* This a Helper class for implementing the equals and the hashcode
* method. Additionally it provides a synchronize method to 'clone'
* one instance into another.
*
* The relation of these three methods is:
* <pre>
* Object o1 = makeObject( "some initializer" );
* Object o2 = makeObject( "another initializer" );
*
* synchronize( o1, o2 );
* assert o1.equals( o2 );
* assert o1.hashcode() == o2.hashcode()
* </pre>
*
* For control of coping and comparing two annotations are provided:
* <tt>@@EqualsClass</tt> and <tt>@@EqualsField</tt>
*
* Additionally JPA annotations are supported:
*
* <tt>@@Transient</tt> and <tt>@@Id</tt> implies <tt>@@EqualsField( include=false )</tt>
*
* These default behaviour can be overwritten by specifying
* <tt>@@EqualsField( include=true )</tt>
*
* @author flo
*
* TODO: "Synchronizable" ist eigentlich vollkommen überflüssig.
* Die Annotations sollte das alleine hinkriegen. Ausserdem ist immer
* noch nicht klar, was nun genau das Interface oder die Annotation auslösen.
* Generell ist das hier noch alles zu chaotisch/unübersichtlich
*
*/
public class Equals {
static final Logger log = LoggerFactory.getLogger( Equals.class );
private static final DefaultSynchroMapper DEFAULT_SYNCHRO_MAPPER = new DefaultSynchroMapper();
/**
* Generate a java hash code for the given object
*
* The object's class must be annotated with @@EqualsClass
*
* @param <T> type of object to process
* @param object to hash
* @return the generated hash code
*/
public static <T> int hash( T object ){
Assert.notNull( object, "object" );
HashCodeBuilder builder = new HashCodeBuilder();
HashVisitor<T> wrapper = new HashVisitor<T>( builder, object );
process( wrapper, null, object );
return builder.toHashCode();
}
/**
* Compares two objects for equality
*
* The objects' class must be annotated with @@EqualsClass
*
* @param <T> type of object to process
* @param o1 First object for equals
* @param o2 Second object for equals
* @return true if both are equal
*/
public static <T> boolean equals( T o1, T o2 ) {
Assert.notNull( o1, "o1" );
if( o2 == null ) return false;
if( o1 == o2 ) return true;
if( o1.getClass() != o2.getClass() ) return false;
EqualsBuilder builder = new EqualsBuilder();
EqualsVisitor<T> wrapper = new EqualsVisitor<T>( builder, o1, o2 );
process( wrapper, null, o1 );
return builder.isEquals();
}
/**
* Generate a strong hash code on the given object
*
* This works in a similar way than hashCode but generates
* a cryptographic strong hash code
*
* @param <T> type of object to process
* @param object the object to hash
* @return a base 64 encoded hash code string
*/
public static <T> String strongHashString( T object ){
return Base64.encodeBase64String( strongHash( object ) ).trim();
}
/**
* Generate a strong hash code on the given object
*
* This works in a similar way than hashCode but generates
* a cryptographic strong hash code
*
* @param <T> type of object to process
* @param object the object to hash
* @return the strong hash code as byte array
*/
public static <T> byte[] strongHash( T object ){
Assert.notNull( object, "object" );
StrongHashCodeBuilder<byte[]> builder = new CryptoHashCodeBuilder();
StrongHashVisitor<byte[],T> wrapper = new StrongHashVisitor<byte[],T>( builder, object );
process( wrapper, null, object );
return builder.toHashCode();
}
/**
* Synchronise 'destination' so that it will equals source.
*
* Formally: <tt>synchronize( o1, o2 ) => equals( o1, o2 ) == true</tt>
*
* This is done 'in place'. No copy of 'target' is created
* However if 'target' is null a new instance will be created using
* the the default constructor.
*
* If custom behaviour is needed a custom SynchroMapper can be provided.
*
* Do this with the the fewest possible (hopefully) amount of fields
*
* You can specify a SynchroMapper which can process the fields which
* are copied. (But not the ones which aren't)
*
* @param <T> type of object to process
* @param destination to synchronise into
* @param source to synchronise from
* @param synchroMapper to do coping or <tt>null</tt> to use DefaultSynchroMapper
* @return the destination (the original, not a copy!)
*/
@SuppressWarnings( "unchecked" )
public static <T> T synchronize(
T destination, T source, SynchroMapper synchroMapper ) {
Assert.notNull( source, "source" );
// Nothing to see here
if( destination == null && source == null ) return null;
T any = source != null ? source : destination;
if( synchroMapper == null ){
if( any instanceof Synchronizable ){
synchroMapper = ((Synchronizable) any).mapper();
} else {
synchroMapper = DEFAULT_SYNCHRO_MAPPER;
}
}
if( destination == null ){
destination = (T)synchroMapper.emptyInstanceOf( null, source );
}
// No synchronisation for equal objects
if( destination == source || destination.equals( source ) ) return destination;
SyncroVisitor<T> wrapper = new SyncroVisitor<T>( destination, source, synchroMapper );
process( wrapper, destination, source );
return destination;
}
private static <T> EnumSet<EqualsOptions.Option> globalOptions( Class<?> clz ){
EnumSet<EqualsOptions.Option> globalOptions = EnumSet.noneOf( EqualsOptions.Option.class );
EqualsOptions equalsOptionA = clz.getAnnotation( EqualsOptions.class );
if( equalsOptionA != null ){
globalOptions = EnumSet.copyOf( Arrays.asList( equalsOptionA.value() ) );
}
return globalOptions;
}
private static <T> void process( Visitor<T> wrapper, T destination, T source ) {
Class<?> sourceClz = source.getClass();
if( destination != null ){
Class<?> destinationClz = destination.getClass();
Assert.equal( destinationClz, "desstination and source class", sourceClz );
}
// Class's annotation
EqualsClass equalsClassA = sourceClz.getAnnotation( EqualsClass.class );
Assert.notNull( equalsClassA, "@EqualsClass for " + sourceClz.getSimpleName() );
EnumSet<EqualsOptions.Option> globalOptions = globalOptions( sourceClz );
// Build accessor list either of fields or methods
List<Accessor> accessors;
switch( equalsClassA.workOn() ){
case FIELDS:{
// Sort methods because order matters for hashCode
Field [] fields = sourceClz.getDeclaredFields();
Arrays.sort( fields, FIELD_SORTER );
accessors = new ArrayList<>( fields.length );
for( Field field : fields ){
accessors.add( new FieldAccessor( field ) );
}
} break;
case METHODS:{
// Sort methods because order matters for hashCode
Method [] methods = sourceClz.getDeclaredMethods();
Arrays.sort( methods, METHOD_SORTER );
accessors = new ArrayList<>( methods.length );
for( Method method : methods ){
accessors.add( new MethodAccessor( sourceClz, method ) );
}
} break;
default:
throw new IllegalArgumentException( "Unknown workOn: " + equalsClassA.workOn() );
}
for( Accessor accessor : accessors ){
if( accessor.isGetable() ){
// Field's annotation
EqualsField equalsFieldA = accessor.getAnnotation( EqualsField.class );
// Field's options + global options
EqualsOptions equalsOptionA = accessor.getAnnotation( EqualsOptions.class );
EnumSet<EqualsOptions.Option> localOptions = EnumSet.copyOf( globalOptions );
if( equalsOptionA != null ){
localOptions.addAll( Arrays.asList( equalsOptionA.value() ) );
}
boolean isInclude;
if( equalsFieldA != null ){
isInclude = equalsFieldA.include();
} else {
isInclude = equalsClassA.select() == Select.ALL;
Transient tAn = accessor.getAnnotation( Transient.class );
Id idAn = accessor.getAnnotation( Id.class );
if( tAn != null ) isInclude = false;
if( idAn != null ) isInclude = false;
}
if(
!accessor.isStatic()
&& ( equalsClassA.includePrivate() || !accessor.isPrivate() )
&& isInclude
){
try {
if( destination != null && !accessor.isSetable() ){
log.warn( "Skipping missing setter for: {}/{}", sourceClz.getSimpleName(), accessor.getName() );
continue;
}
wrapper.invoke( accessor, localOptions );
} catch( Exception e ) {
throw new RuntimeException( "Error in '" + accessor.getName() + "'", e );
}
}
}
}
}
private interface Visitor<T> {
public void invoke( Accessor accessor, EnumSet<EqualsOptions.Option> options ) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, SecurityException;
}
/*
* Uses commons EqualsBuilder to process equals
*/
private static final class EqualsVisitor<T> implements Visitor<T> {
final EqualsBuilder builder;
final T o1, o2;
EqualsVisitor( EqualsBuilder builder, T o1, T o2 ){
this.builder = builder;
this.o1 = o1; this.o2 = o2;
}
@Override
public void invoke( Accessor accessor, EnumSet<EqualsOptions.Option> options )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Object v1 = accessor.get( o1 );
Object v2 = accessor.get( o2 );
v1 = applyOptions( v1, options );
v2 = applyOptions( v2, options );
v1 = reasonable( v1 );
v2 = reasonable( v2 );
builder.append( v1, v2 );
}
}
/*
* Uses commons HashCodeBuilder to generate a hash code
*/
private static final class HashVisitor<T> implements Visitor<T> {
final HashCodeBuilder builder;
final T o;
HashVisitor( HashCodeBuilder builder, T o ){
this.builder = builder;
this.o = o;
}
@Override
public void invoke( Accessor accessor, EnumSet<EqualsOptions.Option> options )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Object value = accessor.get( o );
value = applyOptions( value, options );
value = reasonable( value );
builder.append( value );
}
}
/*
* Uses own strong hash code builder
*/
private static final class StrongHashVisitor<H,T> implements Visitor<T> {
final StrongHashCodeBuilder<H> builder;
final T o;
StrongHashVisitor( StrongHashCodeBuilder<H> builder, T o ){
this.builder = builder;
this.o = o;
}
@Override
public void invoke( Accessor accessor, EnumSet<EqualsOptions.Option> options )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Object value = accessor.get( o );
value = applyOptions( value, options );
value = reasonable( value );
builder.append( value );
}
}
private static final Comparator<Method> METHOD_SORTER = new Comparator<Method>() {
@Override
public int compare( Method o1, Method o2 ) {
return o1.getName().compareTo( o2.getName() );
}
};
private static final Comparator<Field> FIELD_SORTER = new Comparator<Field>() {
@Override
public int compare( Field o1, Field o2 ) {
return o1.getName().compareTo( o2.getName() );
}
};
public static interface Accessor {
public boolean isPrivate();
public boolean isStatic();
public <T extends Annotation> T getAnnotation( Class<T> annotationClass );
public String getName();
public boolean isGetable();
public boolean isSetable();
public Object get( Object o )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException;
public void set( Object o, Object value )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException;
public Class<?> getType();
}
private static class FieldAccessor implements Accessor {
private final Field field;
public FieldAccessor( Field field ) {
this.field = field;
}
@Override
public boolean isPrivate() {
return Modifier.isPrivate( field.getModifiers() );
}
@Override
public boolean isStatic() {
return Modifier.isStatic( field.getModifiers() );
}
@Override
public <T extends Annotation> T getAnnotation( Class<T> annotationClass ){
return field.getAnnotation( annotationClass );
}
@Override
public String getName(){ return field.getName(); }
@Override
public boolean isGetable() { return true; }
@Override
public boolean isSetable() { return true; }
@Override
public Object get( Object o ) throws IllegalArgumentException, IllegalAccessException {
return field.get( o );
}
@Override
public void set( Object o, Object value ) throws IllegalArgumentException, IllegalAccessException {
field.set( o, value );
}
@Override
public Class<?> getType(){
return field.getType();
}
}
private static class MethodAccessor implements Accessor {
private final Class<?> clazz;
private final Method getter;
private Method setter; // cached
private String subname; // cached
public MethodAccessor( Class<?> clazz, Method getter ){
this.clazz = clazz;
this.getter = getter;
}
@Override
public boolean isPrivate() {
return Modifier.isPrivate( getter.getModifiers() );
}
@Override
public boolean isStatic() {
return Modifier.isStatic( getter.getModifiers() );
}
@Override
public <T extends Annotation> T getAnnotation( Class<T> annotationClass ){
return getter.getAnnotation( annotationClass );
}
@Override
public String getName(){
String subname = subname();
if( subname == null )
throw new IllegalArgumentException( "Must start with get/is" );
return tLc( subname );
}
private static final String tLc( String name ){
return name.substring( 0,1 ).toLowerCase() + name.substring( 1 );
}
@Override
public boolean isGetable() {
String subname = subname();
return
subname != null
&& getter.getReturnType() != Void.class
&& getter.getGenericParameterTypes().length == 0
;
}
@Override
public boolean isSetable() {
try {
setter();
} catch( NoSuchMethodException | SecurityException e ) {
return false;
}
return true;
}
private String subname(){
if( subname == null ){
String name = getter.getName();
if( name.startsWith( "get" ) ) subname = name.substring( 3 );
else if( name.startsWith( "is" ) ) subname = name.substring( 2 );
}
return subname;
}
private Method setter() throws NoSuchMethodException, SecurityException{
if( setter == null ){
String subname = subname();
if( subname == null )
throw new NoSuchMethodException( "Must start with get/is" );
String setterName = "set" + subname();
Class<?> getterType = getter.getReturnType();
setter = clazz.getMethod( setterName, getterType );
}
return setter;
}
@Override
public Object get( Object o ) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
return getter.invoke( o );
}
@Override
public void set( Object o, Object value )
throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException {
setter().invoke( o, value );
}
@Override
public Class<?> getType(){
return getter.getReturnType();
}
}
/*
* directly sync on object into another
* uses SynchroMapper to do customisation
*/
private static final class SyncroVisitor<T> implements Visitor<T> {
final T destination;
final T source;
final SynchroMapper sm;
SyncroVisitor( T destination, T source, SynchroMapper mapper ){
this.destination = destination;
this.source = source;
this.sm = mapper;
}
@Override
public void invoke( Accessor accessor, EnumSet<EqualsOptions.Option> options ) throws IllegalArgumentException,
IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, SecurityException {
Object dstVal = accessor.get( destination );
Object srcVal = accessor.get( source );
String name = accessor.getName();
Class<?> type = accessor.getType();
log.debug( "{}, {} <-- {}", new Object[]{ name, "("+type.getSimpleName()+")", dstVal, srcVal } );
dstVal = applyOptions( dstVal, options );
srcVal = applyOptions( srcVal, options );
Object synced = sync( sm, name, dstVal, srcVal );
// Do nothing if values are same
if( synced == dstVal ) return;
// This may be setting value to itself or setting a null to a new value
accessor.set( destination, synced );
}
private static Object sync( SynchroMapper sm, String name, Object dstVal, Object srcVal ){
// this my lead to set( null )
if( srcVal == null ) return null;
// Do nothing if values equal. This prevents setting.
if( srcVal.equals( dstVal ) ) return dstVal;
// Cascade synchronise values
if( isEqualsAnnotated( srcVal ) ){
if( dstVal == null ) dstVal = sm.emptyInstanceOf( name, srcVal );
dstVal = sm.synchronize( name, dstVal, srcVal );
return dstVal;
// "Normal values"
} else if( srcVal instanceof Collection ){
if( dstVal == null )
dstVal = sm.emptyInstanceOf( name, srcVal );
if( dstVal == null )
throw new IllegalArgumentException( "'dstVal' is missing but should have been created beforehand. Perhaps unsupported Set" );
@SuppressWarnings( { "unchecked" } )
Collection<Object>
src = (Collection<Object>)srcVal,
dst = (Collection<Object>)dstVal;
for( Iterator<Object> it = dst.iterator(); it.hasNext(); ){
Object t = it.next();
Object s = sm.find( name, src, t );
if( s == null ){
it.remove();
}
}
for( Iterator<Object> it = src.iterator(); it.hasNext(); ){
Object s = it.next();
Object d = sm.find( name, dst, s );
Object synced = sync( sm, name, d, s );
if( synced != d ) dst.add( synced );
}
// Sort to original order if List
if( src instanceof List ){
List<?> srcList = (List<?>)srcVal,
dstList = (List<?>)dstVal;
Collections.sort( dstList, new List2ListSorter( srcList ) );
}
return dstVal;
// Map --------------------
} else if( srcVal instanceof Map ){
if( dstVal == null )
dstVal = sm.emptyInstanceOf( name, srcVal );
@SuppressWarnings( "unchecked" )
Map<Object,Object>
srcMap = (Map<Object,Object>)srcVal,
dstMap = (Map<Object,Object>)dstVal;
// Remove vanished keys
for( Iterator<?> it = dstMap.keySet().iterator(); it.hasNext(); ){
Object key = it.next();
if( ! srcMap.containsKey( key ) ){
it.remove();
}
}
// Copy all and new keys
for( Object key : srcMap.keySet() ){
Object srcValue = srcMap.get( key );
Object dstValue = dstMap.get( key );
Object synced = sync( sm, name, dstValue, srcValue );
if( synced != dstValue ){
dstMap.put( key, synced );
}
}
return dstMap;
// Simple Value
} else {
return sm.copyOf( name, srcVal );
}
}
}
private static final boolean isEqualsAnnotated( Object o ){
if( o == null ) return false;
return o.getClass().getAnnotation( EqualsClass.class ) != null;
}
/*
* make common adjustments to get this at least less insane
*/
private static final Object reasonable( Object o ){
if( o != null ) {
// Special treatment for currency which has no hashcode method
// This leads to a somewhat changed behaviour since this is stable
// over program runs and calling hashcode isn't.
if( o instanceof Currency ){
o = ( ((Currency)o).getCurrencyCode() );
}
// This is to ignore precision
if( o instanceof BigDecimal ){
o = new Double( ((BigDecimal)o).doubleValue() );
}
// Enums have not stable hash code either
// but a stable string representation
if( o instanceof Enum<?> ) o = ((Enum<?>)o).name();
}
return o;
}
/*
* change value according to options
*/
private static final Object applyOptions( Object o, EnumSet<EqualsOptions.Option> options ){
if( options.contains( EqualsOptions.Option.EMPTY_IS_NULL ) ){
if( o != null ){
if( o instanceof Collection ){
if( ((Collection<?>)o).size() == 0 ) return null;
} else if( o instanceof Map ){
if( ((Map<?,?>)o).size() == 0 ) return null;
} else if( o instanceof String ){
if( ((String)o).length() == 0 ) return null;
}
}
}
return o;
}
/*
* Sort one list so that it has the same object order as the other list
*/
private static final class List2ListSorter implements Comparator<Object> {
List<?> list;
List2ListSorter( List<?> list ){
this.list = list;
}
@Override
public int compare( Object o1, Object o2 ) {
return Integer.valueOf( list.indexOf( o1 ) )
.compareTo( Integer.valueOf( list.indexOf( o2 ) ) );
}
}
}