Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a Case Insensitive DeserializationFeature #568

Merged
merged 2 commits into from
Dec 12, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -378,7 +382,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 @@ -28,6 +28,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 @@ -38,26 +40,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 @@ -75,20 +79,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 @@ -120,7 +124,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 @@ -146,6 +150,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 @@ -216,6 +226,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 @@ -257,7 +272,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 @@ -291,7 +306,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