com.phloc.commons.equals.EqualsImplementationRegistry Maven / Gradle / Ivy
/**
* Copyright (C) 2006-2015 phloc systems
* http://www.phloc.com
* office[at]phloc[dot]com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.phloc.commons.equals;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.phloc.commons.ValueEnforcer;
import com.phloc.commons.annotations.UseDirectEqualsAndHashCode;
import com.phloc.commons.cache.AnnotationUsageCache;
import com.phloc.commons.lang.ClassHelper;
import com.phloc.commons.lang.ClassHierarchyCache;
import com.phloc.commons.lang.ServiceLoaderUtils;
import com.phloc.commons.state.EChange;
/**
* The default implementation of {@link IEqualsImplementationRegistry}.
*
* @author Philip Helger
*/
@ThreadSafe
public final class EqualsImplementationRegistry implements IEqualsImplementationRegistry
{
private static final class ArrayEqualsImplementation implements IEqualsImplementation
{
public ArrayEqualsImplementation ()
{}
public boolean areEqual (final Object aObj1, final Object aObj2)
{
final Object [] aArray1 = (Object []) aObj1;
final Object [] aArray2 = (Object []) aObj2;
// Size check
final int nLength = aArray1.length;
if (nLength != aArray2.length)
return false;
// Content check
for (int i = 0; i < nLength; i++)
if (!EqualsImplementationRegistry.areEqual (aArray1[i], aArray2[i]))
return false;
return true;
}
}
private static final Logger s_aLogger = LoggerFactory.getLogger (EqualsImplementationRegistry.class);
private static final EqualsImplementationRegistry s_aInstance = new EqualsImplementationRegistry ();
private final ReadWriteLock m_aRWLock = new ReentrantReadWriteLock ();
// Use a weak hash map, because the key is a class
private final Map , IEqualsImplementation> m_aMap = new WeakHashMap , IEqualsImplementation> ();
// Cache for classes where direct implementation should be used
private final AnnotationUsageCache m_aDirectEquals = new AnnotationUsageCache (UseDirectEqualsAndHashCode.class);
// Cache for classes that implement equals directly
private final Map m_aImplementsEquals = new HashMap ();
private EqualsImplementationRegistry ()
{
// Register all implementations via SPI
for (final IEqualsImplementationRegistrarSPI aRegistrar : ServiceLoaderUtils.getAllSPIImplementations (IEqualsImplementationRegistrarSPI.class))
aRegistrar.registerEqualsImplementations (this);
}
@Nonnull
public static EqualsImplementationRegistry getInstance ()
{
return s_aInstance;
}
public void registerEqualsImplementation (@Nonnull final Class > aClass, @Nonnull final IEqualsImplementation aImpl)
{
ValueEnforcer.notNull (aClass, "Class");
ValueEnforcer.notNull (aImpl, "Implementation");
if (aClass.equals (Object.class))
throw new IllegalArgumentException ("You cannot provide an equals implementation for Object.class!");
m_aRWLock.writeLock ().lock ();
try
{
final IEqualsImplementation aOldImpl = m_aMap.get (aClass);
if (aOldImpl == null)
m_aMap.put (aClass, aImpl);
else
{
// Avoid the warning when the passed implementation equals the stored
// implementation
if (aOldImpl != aImpl)
s_aLogger.warn ("Another equals implementation for class " +
aClass +
" is already registered (" +
aOldImpl.toString () +
") so it is not overwritten with " +
aImpl.toString ());
}
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
}
@Nonnull
public EChange unregisterEqualsImplementation (@Nonnull final Class > aClass)
{
m_aRWLock.writeLock ().lock ();
try
{
return EChange.valueOf (m_aMap.remove (aClass) != null);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
}
private boolean _isUseDirectEquals (@Nonnull final Class > aClass)
{
return m_aDirectEquals.hasAnnotation (aClass);
}
private boolean _implementsEqualsItself (@Nonnull final Class > aClass)
{
final String sClassName = aClass.getName ();
Boolean aImplementsEqualsItself;
m_aRWLock.readLock ().lock ();
try
{
aImplementsEqualsItself = m_aImplementsEquals.get (sClassName);
}
finally
{
m_aRWLock.readLock ().unlock ();
}
if (aImplementsEqualsItself == null)
{
m_aRWLock.writeLock ().lock ();
try
{
// Try again in write lock
aImplementsEqualsItself = m_aImplementsEquals.get (sClassName);
if (aImplementsEqualsItself == null)
{
// Determine
boolean bRet = false;
try
{
final Method aMethod = aClass.getDeclaredMethod ("equals", Object.class);
if (aMethod != null && aMethod.getReturnType ().equals (boolean.class))
bRet = true;
}
catch (final NoSuchMethodException ex)
{
// ignore
}
aImplementsEqualsItself = Boolean.valueOf (bRet);
m_aImplementsEquals.put (sClassName, aImplementsEqualsItself);
}
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
}
return aImplementsEqualsItself.booleanValue ();
}
@SuppressWarnings ("null")
@Nullable
public IEqualsImplementation getBestMatchingEqualsImplementation (@Nullable final Class > aClass)
{
if (aClass != null)
{
IEqualsImplementation aMatchingImplementation = null;
Class > aMatchingClass = null;
// No check required?
if (_isUseDirectEquals (aClass))
return null;
m_aRWLock.readLock ().lock ();
try
{
// Check for an exact match first
aMatchingImplementation = m_aMap.get (aClass);
if (aMatchingImplementation != null)
aMatchingClass = aClass;
else
{
// Scan hierarchy in most efficient way
for (final WeakReference > aCurWRClass : ClassHierarchyCache.getClassHierarchyIterator (aClass))
{
final Class > aCurClass = aCurWRClass.get ();
if (aCurClass != null)
{
final IEqualsImplementation aImpl = m_aMap.get (aCurClass);
if (aImpl != null)
{
aMatchingImplementation = aImpl;
aMatchingClass = aCurClass;
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("Found hierarchical match with class " +
aMatchingClass +
" when searching for " +
aClass);
break;
}
}
}
}
}
finally
{
m_aRWLock.readLock ().unlock ();
}
// Do this outside of the lock for performance reasons
if (aMatchingImplementation != null)
{
// If the matching implementation is for an interface and the
// implementation class implements equals, use the one from the class
// Example: a converter for "Map" is registered, but "LRUCache" comes
// with its own "equals" implementation
if (ClassHelper.isInterface (aMatchingClass) && _implementsEqualsItself (aClass))
{
// Remember to use direct implementation
m_aDirectEquals.setAnnotation (aClass, true);
return null;
}
if (!aMatchingClass.equals (aClass))
{
// We found a match by walking the hierarchy -> put that match in the
// direct hit list for further speed up
registerEqualsImplementation (aClass, aMatchingImplementation);
}
return aMatchingImplementation;
}
// Handle arrays specially, because we cannot register a converter for
// every potential array class (but we allow for special implementations)
if (ClassHelper.isArrayClass (aClass))
return new ArrayEqualsImplementation ();
// Remember to use direct implementation
m_aDirectEquals.setAnnotation (aClass, true);
}
// No special handler found
if (s_aLogger.isDebugEnabled ())
s_aLogger.debug ("Found no equals implementation for " + aClass);
// Definitely no special implementation
return null;
}
public static boolean areEqual (@Nullable final T aObj1, @Nullable final T aObj2)
{
// Same object - check first
if (aObj1 == aObj2)
return true;
// Is only one value null?
if (aObj1 == null || aObj2 == null)
return false;
// Check whether the implementation classes are identical
final Class > aClass1 = aObj1.getClass ();
final Class > aClass2 = aObj2.getClass ();
if (!aClass1.equals (aClass2))
{
// Not the same class -> not equal!
return false;
}
// Same class
final IEqualsImplementation aImpl = s_aInstance.getBestMatchingEqualsImplementation (aClass1);
// Start the main equals check
boolean bAreEqual;
if (aImpl == null)
{
// No special implementation found
bAreEqual = aObj1.equals (aObj2);
}
else
{
// A special implementation was found!
bAreEqual = aImpl.areEqual (aObj1, aObj2);
}
return bAreEqual;
}
public static void clearCache ()
{
s_aInstance.m_aDirectEquals.clearCache ();
}
}