CacheKey.Equals(object)/HashCode issue still a problem (was IBATISNET-118)
--------------------------------------------------------------------------
Key: IBATISNET-119
URL: http://issues.apache.org/jira/browse/IBATISNET-119
Project: iBatis for .NET
Type: Bug
Components: DataMapper
Versions: DataMapper 1.2.1
Environment: Windows
Reporter: Thomas Tannahill
The fix for IBATISNET-118 does not address the issue (just hides it in one
particular instance).
The issue is that you can not use HashCodes to determine object equality.
Unique objects do not always have unique HashCodes (no matter how hard you
try). There just aren't enough hashcodes to go around :)
I understand why my previous 'patch' wasn't accepted (didn't expect it to be).
Since we are not using an object's own "GetHashCode()" to get it's hashcode, we
can not use it's own "Equals" to determine equality. So we really have to do
is create a new method on ObjectProbe that compares two objects in much the
same way that ObjectProbe generates hashcodes. I was just too lazy to write it
yesterday :)
Here is an updated unit test and a potential fix...
--- BEGIN UNIT TEST ---
using IBatisNet.DataMapper;
using IBatisNet.DataMapper.TypeHandlers;
using NUnit.Framework;
namespace IBatisNet.DataMapper.Test.NUnit.SqlMapTests
{
[TestFixture]
public class CacheKeyTest
{
[Test]
public void
ShouldNotConsider1LAndNegative9223372034707292159LToBeEqual()
{
// old version of ObjectProbe gave TestClass based on
these longs the same HashCode
DoTestClassEquals(1L, -9223372034707292159L);
}
[Test]
public void
ShouldNotConsider1LAndNegative9223372036524971138LToBeEqual()
{
// current version of ObjectProbe gives TestClass based
on these longs the same HashCode
DoTestClassEquals(1L, -9223372036524971138L);
}
private static void DoTestClassEquals(long firstLong, long
secondLong)
{
TypeHandlerFactory factory = new TypeHandlerFactory();
// Two cache keys are equal except for the parameter.
CacheKey key = new CacheKey(factory, "STATEMENT",
"SQL", new TestClass(firstLong), new string[] {"AProperty"}, 0, 0,
CacheKeyType.Object);
CacheKey aDifferentKey = new CacheKey(factory,
"STATEMENT", "SQL", new TestClass(secondLong), new string[] {"AProperty"}, 0,
0, CacheKeyType.Object);
Assert.IsFalse(aDifferentKey.Equals(key)); // should
not be equal.
}
private class TestClass
{
private long _property = long.MinValue;
public TestClass(long aProperty)
{
_property = aProperty;
}
public long AProperty
{
get { return _property; }
set { _property = value; }
}
}
}
}
--- END UNIT TEST ---
FIX (not sure if this will break other parts of the system... but it shouldn't):
--- BEGIN ObjectProbe ---
#region Apache Notice
/*****************************************************************************
* $Header: $
* $Revision: $
* $Date: $
*
* iBATIS.NET Data Mapper
* Copyright (C) 2004 - Gilles Bayon
*
*
* 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.
*
********************************************************************************/
#endregion
using System;
using System.Collections;
using System.Reflection;
using IBatisNet.Common.Exceptions;
namespace IBatisNet.Common.Utilities.Objects
{
/// <summary>
/// Description résumée de ObjectProbe.
/// </summary>
public class ObjectProbe
{
private static ArrayList _simpleTypeMap = new ArrayList();
static ObjectProbe()
{
_simpleTypeMap.Add(typeof(string));
_simpleTypeMap.Add(typeof(Byte));
_simpleTypeMap.Add(typeof(Int16));
_simpleTypeMap.Add(typeof(char));
_simpleTypeMap.Add(typeof(Int32));
_simpleTypeMap.Add(typeof(Int64));
_simpleTypeMap.Add(typeof(Single));
_simpleTypeMap.Add(typeof(Double));
_simpleTypeMap.Add(typeof(Boolean));
_simpleTypeMap.Add(typeof(DateTime));
_simpleTypeMap.Add(typeof(Decimal));
//
_simpleTypeMap.Add(typeof(Hashtable));
//
_simpleTypeMap.Add(typeof(SortedList));
//
_simpleTypeMap.Add(typeof(ArrayList));
//
_simpleTypeMap.Add(typeof(Array));
//
simpleTypeMap.Add(LinkedList.class);
//
simpleTypeMap.Add(HashSet.class);
//
simpleTypeMap.Add(TreeSet.class);
// simpleTypeMap.Add(Vector.class);
//
simpleTypeMap.Add(Hashtable.class);
_simpleTypeMap.Add(typeof(SByte));
_simpleTypeMap.Add(typeof(UInt16));
_simpleTypeMap.Add(typeof(UInt32));
_simpleTypeMap.Add(typeof(UInt64));
_simpleTypeMap.Add(typeof(IEnumerator));
}
/// <summary>
/// Returns an array of the readable properties names exposed
by an object
/// </summary>
/// <param name="obj">The object</param>
/// <returns>The properties name</returns>
public static string[] GetReadablePropertyNames(object obj)
{
return
ReflectionInfo.GetInstance(obj.GetType()).GetReadablePropertyNames();
}
/// <summary>
/// Returns an array of the writeable properties name exposed
by a object
/// </summary>
/// <param name="obj">The object</param>
/// <returns>The properties name</returns>
public static string[] GetWriteablePropertyNames(object obj)
{
return
ReflectionInfo.GetInstance(obj.GetType()).GetWriteablePropertyNames();
}
/// <summary>
/// Returns the type that the set expects to receive as a
parameter when
/// setting a property value.
/// </summary>
/// <param name="obj">The object to check</param>
/// <param name="propertyName">The name of the property</param>
/// <returns>The type of the property</returns>
private static Type GetPropertyTypeForSetter(object obj, string
propertyName)
{
Type type = obj.GetType();
if (obj is IDictionary)
{
IDictionary map = (IDictionary) obj;
object value = map[propertyName];
if (value == null)
{
type = typeof(object);
}
else
{
type = value.GetType();
}
}
else
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator =
parser.GetEnumerator();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetSetterType(propertyName);
}
}
else
{
type =
ReflectionInfo.GetInstance(type).GetSetterType(propertyName);
}
}
return type;
}
/// <summary>
/// Returns the type that the set expects to receive as a
parameter when
/// setting a property value.
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="propertyName">The name of the property</param>
/// <returns>The type of the property</returns>
private static Type GetPropertyTypeForSetter(Type type, string
propertyName)
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator = parser.GetEnumerator();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetSetterType(propertyName);
}
}
else
{
type =
ReflectionInfo.GetInstance(type).GetSetterType(propertyName);
}
return type;
}
/// <summary>
/// Returns the type that the get expects to receive as a
parameter when
/// setting a property value.
/// </summary>
/// <param name="obj">The object to check</param>
/// <param name="propertyName">The name of the property</param>
/// <returns>The type of the property</returns>
public static Type GetPropertyTypeForGetter(object obj, string
propertyName)
{
Type type = obj.GetType();
if (obj is IDictionary)
{
IDictionary map = (IDictionary) obj;
object value = map[propertyName];
if (value == null)
{
type = typeof(object);
}
else
{
type = value.GetType();
}
}
else
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator =
parser.GetEnumerator();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
}
}
else
{
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
}
}
return type;
}
/// <summary>
/// Returns the type that the get expects to receive as a
parameter when
/// setting a property value.
/// </summary>
/// <param name="type">The type to check</param>
/// <param name="propertyName">The name of the property</param>
/// <returns>The type of the property</returns>
public static Type GetPropertyTypeForGetter(Type type, string
propertyName)
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator = parser.GetEnumerator();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
}
}
else
{
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
}
return type;
}
private static object GetArrayProperty(object obj, string
indexedName)
{
object value = null;
try
{
int startIndex = indexedName.IndexOf("[");
int length = indexedName.IndexOf("]");
string name = indexedName.Substring(0,
startIndex);
string index = indexedName.Substring(
startIndex+1, length-(startIndex+1));
int i = System.Convert.ToInt32(index);
if (name.Length > 0)
{
value = GetProperty(obj, name);
}
else
{
value = obj;
}
if (value is IList)
{
value = ((IList) value)[i];
}
else
{
throw new ProbeException("The '" + name
+ "' property of the " + obj.GetType().Name + " class is not a List or Array.");
}
}
catch (ProbeException pe)
{
throw pe;
}
catch(Exception e)
{
throw new ProbeException("Error getting ordinal
value from .net object. Cause" + e.Message, e);
}
return value;
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
protected static object GetProperty(object obj, string
propertyName)
{
ReflectionInfo reflectionCache =
ReflectionInfo.GetInstance(obj.GetType());
try
{
object value = null;
if (propertyName.IndexOf("[") > -1)
{
value = GetArrayProperty(obj,
propertyName);
}
else
{
if (obj is IDictionary)
{
value = ((IDictionary)
obj)[propertyName];
}
else
{
PropertyInfo propertyInfo =
reflectionCache.GetGetter(propertyName);
if (propertyInfo == null)
{
throw new
ProbeException("No Get method for property " + propertyName + " on instance of
" + obj.GetType().Name);
}
try
{
value =
propertyInfo.GetValue(obj, null);
}
catch (ArgumentException ae)
{
throw new
ProbeException(ae);
}
catch (TargetException t)
{
throw new
ProbeException(t);
}
catch
(TargetParameterCountException tp)
{
throw new
ProbeException(tp);
}
catch (MethodAccessException
ma)
{
throw new
ProbeException(ma);
}
}
}
return value;
}
catch (ProbeException pe)
{
throw pe;
}
catch(Exception e)
{
throw new ProbeException("Could not Set
property '" + propertyName + "' for " + obj.GetType().Name + ". Cause: " +
e.Message, e);
}
}
private static void SetArrayProperty(object obj, string
indexedName, object value)
{
try
{
int startIndex = indexedName.IndexOf("[");
int length = indexedName.IndexOf("]");
string name = indexedName.Substring(0,
startIndex);
string index = indexedName.Substring(
startIndex+1, length-(startIndex+1));
int i = System.Convert.ToInt32(index);
object list = null;
if (name.Length > 0)
{
list = GetProperty(obj, name);
}
else
{
list = obj;
}
if (list is IList)
{
((IList) list)[i] = value;
}
else
{
throw new ProbeException("The '" + name
+ "' property of the " + obj.GetType().Name + " class is not a List or Array.");
}
}
catch (ProbeException pe)
{
throw pe;
}
catch (Exception e)
{
throw new ProbeException("Error getting ordinal
value from .net object. Cause" + e.Message, e);
}
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <param name="propertyName"></param>
/// <param name="propertyValue"></param>
protected static void SetProperty(object obj, string
propertyName, object propertyValue)
{
ReflectionInfo reflectionCache =
ReflectionInfo.GetInstance(obj.GetType());
try
{
if (propertyName.IndexOf("[") > -1)
{
SetArrayProperty(obj, propertyName,
propertyValue);
}
else
{
if (obj is IDictionary)
{
((IDictionary)
obj)[propertyName] = propertyValue;
}
else
{
PropertyInfo propertyInfo =
reflectionCache.GetSetter(propertyName);
if (propertyInfo == null)
{
throw new
ProbeException("No Set method for property " + propertyName + " on instance of
" + obj.GetType().Name);
}
try
{
propertyInfo.SetValue(obj, propertyValue, null);
}
catch (ArgumentException ae)
{
throw new
ProbeException(ae);
}
catch (TargetException t)
{
throw new
ProbeException(t);
}
catch
(TargetParameterCountException tp)
{
throw new
ProbeException(tp);
}
catch (MethodAccessException
ma)
{
throw new
ProbeException(ma);
}
}
}
}
catch (ProbeException pe)
{
throw pe;
}
catch (Exception e)
{
throw new ProbeException("Could not Get
property '" + propertyName + "' for " + obj.GetType().Name + ". Cause: " +
e.Message, e);
}
}
/// <summary>
/// Return the specified property on an object.
/// </summary>
/// <param name="obj">The Object on which to invoke the
specified property.</param>
/// <param name="propertyName">The name of the property.</param>
/// <returns>An Object representing the return value of the
invoked property.</returns>
public static object GetPropertyValue(object obj, string
propertyName)
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator = parser.GetEnumerator();
object value = obj;
string token = null;
while (enumerator.MoveNext())
{
token = (string)enumerator.Current;
value = GetProperty(value, token);
if (value == null)
{
break;
}
}
return value;
}
else
{
return GetProperty(obj, propertyName);
}
}
/// <summary>
/// Set the specified property on an object
/// </summary>
/// <param name="obj">The Object on which to invoke the
specified property.</param>
/// <param name="propertyName">The name of the property to
set.</param>
/// <param name="propertyValue">The new value to set.</param>
public static void SetPropertyValue(object obj, string
propertyName, object propertyValue)
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator = parser.GetEnumerator();
enumerator.MoveNext();
string currentPropertyName =
(string)enumerator.Current;
object child = obj;
while (enumerator.MoveNext())
{
Type type =
GetPropertyTypeForSetter(child, currentPropertyName);
object parent = child;
child = GetProperty(parent,
currentPropertyName);
if (child == null)
{
try
{
child =
Activator.CreateInstance(type);
SetPropertyValue(parent, currentPropertyName, child);
}
catch (Exception e)
{
throw new
ProbeException("Cannot set value of property '" + propertyName + "' because '"
+ currentPropertyName + "' is null and cannot be instantiated on instance of "
+ type.Name + ". Cause:" + e.Message, e);
}
}
currentPropertyName =
(string)enumerator.Current;
}
SetProperty(child, currentPropertyName,
propertyValue);
}
else
{
SetProperty(obj, propertyName, propertyValue);
}
}
/// <summary>
/// Checks to see if a Object has a writable property/field be
a given name
/// </summary>
/// <param name="obj"> The object to check</param>
/// <param name="propertyName">The property to check for</param>
/// <returns>True if the property exists and is
writable</returns>
public static bool HasWritableProperty(object obj, string
propertyName)
{
bool hasProperty = false;
if (obj is IDictionary)
{
hasProperty = ((IDictionary)
obj).Contains(propertyName);
}
else
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator =
parser.GetEnumerator();
Type type = obj.GetType();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
hasProperty =
ReflectionInfo.GetInstance(type).HasWritableProperty(propertyName);
}
}
else
{
hasProperty =
ReflectionInfo.GetInstance(obj.GetType()).HasWritableProperty(propertyName);
}
}
return hasProperty;
}
/// <summary>
/// Checks to see if the Object have a property/field be a
given name.
/// </summary>
/// <param name="obj">The Object on which to invoke the
specified property.</param>
/// <param name="propertyName">The name of the property to
check for.</param>
/// <returns>
/// True or false if the property exists and is readable.
/// </returns>
public static bool HasReadableProperty(object obj, string
propertyName)
{
bool hasProperty = false;
if (obj is IDictionary)
{
hasProperty = ((IDictionary)
obj).Contains(propertyName);
}
else
{
if (propertyName.IndexOf('.') > -1)
{
StringTokenizer parser = new
StringTokenizer(propertyName, ".");
IEnumerator enumerator =
parser.GetEnumerator();
Type type = obj.GetType();
while (enumerator.MoveNext())
{
propertyName =
(string)enumerator.Current;
type =
ReflectionInfo.GetInstance(type).GetGetterType(propertyName);
hasProperty =
ReflectionInfo.GetInstance(type).HasReadableProperty(propertyName);
}
}
else
{
hasProperty =
ReflectionInfo.GetInstance(obj.GetType()).HasReadableProperty(propertyName);
}
}
return hasProperty;
}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsSimpleType(Type type)
{
if (_simpleTypeMap.Contains(type))
{
return true;
}
else if (type.IsSubclassOf(typeof(ICollection)))
{
return true;
}
else if (type.IsSubclassOf(typeof(IDictionary)))
{
return true;
}
else if (type.IsSubclassOf(typeof(IList)))
{
return true;
}
else if (type.IsSubclassOf(typeof(IEnumerable)))
{
return true;
}
else
{
return false;
}
}
/// <summary>
/// Calculates a hash code for all readable properties of a
object.
/// </summary>
/// <param name="obj">The object to calculate the hash code
for.</param>
/// <returns>The hash code.</returns>
public static int ObjectHashCode(object obj)
{
return ObjectHashCode(obj,
GetReadablePropertyNames(obj));
}
public static int ObjectHashCode(object obj, string[]
properties)
{
ArrayList values = UnwrapObjectDownToSimpleTypes(obj,
properties);
int hashCode = obj.GetType().FullName.GetHashCode();
foreach (object simpleObject in values)
{
if (simpleObject != null)
{
hashCode += simpleObject.GetHashCode();
hashCode +=
simpleObject.ToString().GetHashCode();
hashCode *= 29;
}
}
return hashCode;
}
public static bool AreObjectsEqual(object obj1, object obj2)
{
return AreObjectsEqual(obj1, obj2,
GetReadablePropertyNames(obj1));
}
public static bool AreObjectsEqual(object obj1, object obj2,
string[] properties)
{
if (obj1 == null && obj2 != null)
return false;
if (obj1 != null && obj2 == null)
return false;
if (obj1 == null && obj2 == null)
return true;
if (obj1.GetType() != obj2.GetType())
return false;
ArrayList obj1Values =
UnwrapObjectDownToSimpleTypes(obj1, properties);
ArrayList obj2Values =
UnwrapObjectDownToSimpleTypes(obj2, properties);
if (obj1Values.Count != obj2Values.Count)
return false;
for (int i = 0; i < obj1Values.Count; i++)
{
if (obj1Values[i] != obj2Values[i])
return false;
}
return true;
}
public static ArrayList UnwrapObjectDownToSimpleTypes(object
obj, ArrayList objectValues)
{
return UnwrapObjectDownToSimpleTypes(obj,
GetReadablePropertyNames(obj), objectValues);
}
public static ArrayList UnwrapObjectDownToSimpleTypes(object
obj, string[] properties)
{
return UnwrapObjectDownToSimpleTypes(obj, properties,
new ArrayList());
}
public static ArrayList UnwrapObjectDownToSimpleTypes(object
obj, string[] properties, ArrayList objectValues)
{
ArrayList alreadyDigested = new ArrayList();
int hashcode = obj.GetType().FullName.GetHashCode();
for (int i = 0; i < properties.Length; i++)
{
object value = GetProperty(obj, properties[i]);
if (value != null)
{
if (IsSimpleType(value.GetType()))
{
objectValues.Add(value);
}
else
{
// It's a Object
// Check to avoid endless loop
(circular dependency)
if (value != obj)
{
if
(!alreadyDigested.Contains(value))
{
alreadyDigested.Add(value);
UnwrapObjectDownToSimpleTypes(value, objectValues);
}
}
}
}
else
{
objectValues.Add(null);
}
}
return objectValues;
}
}
}
--- END ObjectProbe ---
--- BEGIN CacheKey.Equals(object) ---
public override bool Equals(object obj)
{
//-----------------------------------
if (this == obj) return true;
if (!(obj is CacheKey)) return false;
CacheKey cacheKey = (CacheKey)obj;
if (_maxResults != cacheKey._maxResults) return false;
if (_skipRecords != cacheKey._skipRecords) return false;
if (_type != cacheKey._type) return false;
if (_parameter is Hashtable)
{
if (_hashCode != cacheKey._hashCode) return
false;
if (!_parameter.Equals(cacheKey._parameter))
return false;
}
else if (_parameter != null &&
_typeHandlerFactory.IsSimpleType(_parameter.GetType()))
{
if (_parameter != null ?
!_parameter.Equals(cacheKey._parameter) : cacheKey._parameter != null) return
false;
}
else
{
if (!ObjectProbe.AreObjectsEqual(_parameter,
cacheKey._parameter, _properties))
{
return false;
}
}
if (_sql != null ? !_sql.Equals(cacheKey._sql) :
cacheKey._sql != null) return false;
return true;
}
--- END CacheKey.Equals(object) ---
--
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators:
http://issues.apache.org/jira/secure/Administrators.jspa
-
For more information on JIRA, see:
http://www.atlassian.com/software/jira