Skip to content

Commit

Permalink
Merge pull request #568 from mressler/master
Browse files Browse the repository at this point in the history
Adding a Case Insensitive DeserializationFeature
  • Loading branch information
cowtowncoder committed Dec 12, 2014
2 parents df9b129 + edad12f commit 9144f66
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 13 deletions.
11 changes: 10 additions & 1 deletion src/main/java/com/fasterxml/jackson/databind/MapperFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,16 @@ public enum MapperFeature implements ConfigFeature
/* Name-related features
/******************************************************
*/


/**
* Feature that will allow for more forgiving deserialization of incoming JSON.
* If enabled, the bean properties will be matched using their lower-case
* equivalents.
* <p>
* Feature is disabled by default.
*/
ACCEPT_CASE_INSENSITIVE_PROPERTIES(false),

/**
* Feature that can be enabled to make property names be
* overridden by wrapper name (usually detected with annotations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class BeanDeserializerBuilder

final protected boolean _defaultViewInclusion;

final protected boolean _caseInsensitivePropertyComparison;

/*
/**********************************************************
/* Accumulated information about properties
Expand Down Expand Up @@ -99,6 +101,7 @@ public BeanDeserializerBuilder(BeanDescription beanDesc,
{
_beanDesc = beanDesc;
_defaultViewInclusion = config.isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION);
_caseInsensitivePropertyComparison = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
}

/**
Expand All @@ -109,6 +112,7 @@ protected BeanDeserializerBuilder(BeanDeserializerBuilder src)
{
_beanDesc = src._beanDesc;
_defaultViewInclusion = src._defaultViewInclusion;
_caseInsensitivePropertyComparison = src._caseInsensitivePropertyComparison;

// let's make copy of properties
_properties.putAll(src._properties);
Expand Down Expand Up @@ -313,7 +317,7 @@ public JsonPOJOBuilder.Value getBuilderConfig() {
public JsonDeserializer<?> build()
{
Collection<SettableBeanProperty> props = _properties.values();
BeanPropertyMap propertyMap = new BeanPropertyMap(props);
BeanPropertyMap propertyMap = new BeanPropertyMap(props, _caseInsensitivePropertyComparison);
propertyMap.assignIndexes();

// view processing must be enabled if:
Expand Down Expand Up @@ -377,7 +381,7 @@ public JsonDeserializer<?> buildBuilderBased(JavaType valueType,
}
// And if so, we can try building the deserializer
Collection<SettableBeanProperty> props = _properties.values();
BeanPropertyMap propertyMap = new BeanPropertyMap(props);
BeanPropertyMap propertyMap = new BeanPropertyMap(props, _caseInsensitivePropertyComparison);
propertyMap.assignIndexes();

boolean anyViews = !_defaultViewInclusion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public final class BeanPropertyMap
private final int _hashMask;

private final int _size;

private final boolean _caseInsensitivePropertyComparison;

/**
* Counter we use to keep track of insertion order of properties
Expand All @@ -41,26 +43,28 @@ public final class BeanPropertyMap
*/
private int _nextBucketIndex = 0;

public BeanPropertyMap(Collection<SettableBeanProperty> properties)
public BeanPropertyMap(Collection<SettableBeanProperty> properties, boolean caseInsensitivePropertyComparison)
{
_caseInsensitivePropertyComparison = caseInsensitivePropertyComparison;
_size = properties.size();
int bucketCount = findSize(_size);
_hashMask = bucketCount-1;
Bucket[] buckets = new Bucket[bucketCount];
for (SettableBeanProperty property : properties) {
String key = property.getName();
String key = getPropertyName(property);
int index = key.hashCode() & _hashMask;
buckets[index] = new Bucket(buckets[index], key, property, _nextBucketIndex++);
}
_buckets = buckets;
}

private BeanPropertyMap(Bucket[] buckets, int size, int index)
private BeanPropertyMap(Bucket[] buckets, int size, int index, boolean caseInsensitivePropertyComparison)
{
_buckets = buckets;
_size = size;
_hashMask = buckets.length-1;
_nextBucketIndex = index;
_caseInsensitivePropertyComparison = caseInsensitivePropertyComparison;
}

/**
Expand All @@ -78,20 +82,20 @@ public BeanPropertyMap withProperty(SettableBeanProperty newProperty)
final int bcount = _buckets.length;
Bucket[] newBuckets = new Bucket[bcount];
System.arraycopy(_buckets, 0, newBuckets, 0, bcount);
final String propName = newProperty.getName();
final String propName = getPropertyName(newProperty);
// and then see if it's add or replace:
SettableBeanProperty oldProp = find(newProperty.getName());
SettableBeanProperty oldProp = find(propName);
if (oldProp == null) { // add
// first things first: add or replace?
// can do a straight copy, since all additions are at the front
// and then insert the new property:
int index = propName.hashCode() & _hashMask;
newBuckets[index] = new Bucket(newBuckets[index],
propName, newProperty, _nextBucketIndex++);
return new BeanPropertyMap(newBuckets, _size+1, _nextBucketIndex);
return new BeanPropertyMap(newBuckets, _size+1, _nextBucketIndex, _caseInsensitivePropertyComparison);
}
// replace: easy, close + replace
BeanPropertyMap newMap = new BeanPropertyMap(newBuckets, bcount, _nextBucketIndex);
BeanPropertyMap newMap = new BeanPropertyMap(newBuckets, bcount, _nextBucketIndex, _caseInsensitivePropertyComparison);
newMap.replace(newProperty);
return newMap;
}
Expand Down Expand Up @@ -123,7 +127,7 @@ public BeanPropertyMap renameAll(NameTransformer transformer)
newProps.add(prop);
}
// should we try to re-index? Ordering probably changed but called probably doesn't want changes...
return new BeanPropertyMap(newProps);
return new BeanPropertyMap(newProps, _caseInsensitivePropertyComparison);
}

public BeanPropertyMap assignIndexes()
Expand All @@ -149,6 +153,12 @@ private final static int findSize(int size)
}
return result;
}

// Confining this case insensitivity to this function (and the find method) in case we want to
// apply a particular locale to the lower case function. For now, using the default.
private String getPropertyName(SettableBeanProperty prop) {
return _caseInsensitivePropertyComparison ? prop.getName().toLowerCase() : prop.getName();
}

/*
/**********************************************************
Expand Down Expand Up @@ -219,6 +229,11 @@ public SettableBeanProperty find(String key)
if (key == null) {
throw new IllegalArgumentException("Can not pass null property name");
}

if (_caseInsensitivePropertyComparison) {
key = key.toLowerCase();
}

int index = key.hashCode() & _hashMask;
Bucket bucket = _buckets[index];
// Let's unroll first lookup since that is null or match in 90+% cases
Expand Down Expand Up @@ -298,7 +313,7 @@ public SettableBeanProperty find(int propertyIndex)
*/
public void replace(SettableBeanProperty property)
{
String name = property.getName();
String name = getPropertyName(property);
int index = name.hashCode() & (_buckets.length-1);

/* This is bit tricky just because buckets themselves
Expand Down Expand Up @@ -332,7 +347,7 @@ public void replace(SettableBeanProperty property)
public void remove(SettableBeanProperty property)
{
// Mostly this is the same as code with 'replace', just bit simpler...
String name = property.getName();
String name = getPropertyName(property);
int index = name.hashCode() & (_buckets.length-1);
Bucket tail = null;
boolean found = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,33 @@ public void testPOJOFromEmptyArray() throws Exception
Bean result = r.readValue(JSON);
assertNull(result);
}

// [Databind#566]
public void testCaseInsensitiveDeserialization() throws Exception
{
final String JSON = "{\"Value1\" : {\"nAme\" : \"fruit\", \"vALUe\" : \"apple\"}, \"valUE2\" : {\"NAME\" : \"color\", \"value\" : \"red\"}}";

// first, verify default settings which do not accept improper case
ObjectMapper mapper = new ObjectMapper();
assertFalse(mapper.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));

try {
mapper.readValue(JSON, Issue476Bean.class);

fail("Should not accept improper case properties by default");
} catch (JsonProcessingException e) {
verifyException(e, "Unrecognized field");
assertValidLocation(e.getLocation());
}

// Definitely not OK to enable dynamically - the BeanPropertyMap (which is the consumer of this particular feature) gets cached.
mapper = new ObjectMapper();
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
ObjectReader r = mapper.reader(Issue476Bean.class);
Issue476Bean result = r.readValue(JSON);
assertEquals(result.value1.name, "fruit");
assertEquals(result.value1.value, "apple");
}

// [Issue#120]
public void testModifyArrayDeserializer() throws Exception
Expand Down

0 comments on commit 9144f66

Please sign in to comment.