Pages

Friday, March 21, 2008

CaseInsensitiveMap and CollectionFactory

The code of the subject:
  Map map = ...;
map.put("hello", "world")
System.out.println(map.get("hello"));
System.out.println(map.get("HELLO"));
The output:
  world
world
How could this possibly work?
In the real code, the map was received as a result from org.springframework.jdbc.core.JdbcTemplate#queryForList() method call, and SpringFramework 2.0.8 is in use. As it came out, the ResultSet extraction spans down to the org.springframework.core.CollectionFactory class which purpose is
... to avoid runtime dependencies on JDK 1.4+ or Commons Collections 3.x, simply using the best collection implementation that is available.

That said, there's a dependency on Commons Collections 3.x library. The while extracting the ResultSet received from the query call, the framework asks the factory for the map implementation to accumulate the results and makes a call to CollectionFactory#createLinkedCaseInsensitiveMapIfPossible() method:

private static final boolean commonsCollections3Available =
ClassUtils.isPresent("org.apache.commons.collections.map.LinkedMap",
CollectionFactory.class.getClassLoader());
...
public static Map createLinkedCaseInsensitiveMapIfPossible(int initialCapacity) {
if (commonsCollections3Available) {
logger.trace("Creating [org.apache.commons.collections.map.ListOrderedMap/CaseInsensitiveMap]");
return CommonsCollectionFactory.createListOrderedCaseInsensitiveMap(initialCapacity);
}
else if (JdkVersion.isAtLeastJava14()) {
logger.debug("Falling back to [java.util.LinkedHashMap] for linked case-insensitive map");
return JdkCollectionFactory.createLinkedHashMap(initialCapacity);
}
else {
logger.debug("Falling back to plain [java.util.HashMap] for linked case-insensitive map");
return new HashMap(initialCapacity);
}
}
...
private static Map createListOrderedCaseInsensitiveMap(int initialCapacity) {
// Commons Collections does not support initial capacity of 0.
return ListOrderedMap.decorate(
new CaseInsensitiveMap(
initialCapacity == 0 ? 1 :
initialCapacity));
}

So, if the Commons Collections 3.x exists in the classpath, we'll receive a CaseInsensitiveMap decorated with ListOrderedMap to preserve the order in which the objects would be isnerted into the map. In its turn, CaseInsensitiveMap stores calls the Object.toString().toLowerCase() method on any key object that is being added, so the keys are always lowercase strings! And this is why the example above was working.

The problem is that if you won't add the Commons Collections library to classpath, and will use lowercase strings to pick the values (i.e. map.get("some_key")), then you will most probably get null as a result, because there's absolutely no guarantee that the column name in the database is in lowercase.

No comments:

Disqus for Code Impossible