DATAMONGO-380 - Improved map handling for keys containing dots.

MappingMongoConverter now rejects objects that would result in field keys containing a dot as we cannot reliably escape and unescape them without potentially wrecking correct keys on reading. However I added a property mapKeyReplacement that can be set to e.g. ~ to have all dots in map keys replaced with ~. This will of course cause ~ to be transformed into dots when reading. If further customization is necessary override potentiallyEscapeMapKey(…) and potentiallyUnescapeMapKey(…).
This commit is contained in:
Oliver Gierke
2012-02-10 18:15:23 +01:00
parent 8f6d940036
commit 40236d4099
2 changed files with 89 additions and 3 deletions

View File

@@ -81,6 +81,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
protected ApplicationContext applicationContext;
protected boolean useFieldAccessOnly = true;
protected MongoTypeMapper typeMapper;
protected String mapKeyDotReplacement = null;
/**
* Creates a new {@link MappingMongoConverter} given the new {@link MongoDbFactory} and {@link MappingContext}.
@@ -116,6 +117,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
mappingContext) : typeMapper;
}
/**
* Configure the characters dots potentially contained in a {@link Map} shall be replaced with. By default we don't do
* any translation but rather reject a {@link Map} with keys containing dots causing the conversion for the entire
* object to fail. If further customization of the translation is needed, have a look at
* {@link #potentiallyEscapeMapKey(String)} as well as {@link #potentiallyUnescapeMapKey(String)}.
*
* @param mapKeyDotReplacement the mapKeyDotReplacement to set
*/
public void setMapKeyDotReplacement(String mapKeyDotReplacement) {
this.mapKeyDotReplacement = mapKeyDotReplacement;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityConverter#getMappingContext()
@@ -496,7 +509,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.isSimpleType(key.getClass())) {
// Don't use conversion service here as removal of ObjectToString converter results in some primitive types not
// being convertable
String simpleKey = key.toString();
String simpleKey = potentiallyEscapeMapKey(key.toString());
if (val == null || conversions.isSimpleType(val.getClass())) {
writeSimpleInternal(val, dbo, simpleKey);
} else if (val instanceof Collection || val.getClass().isArray()) {
@@ -517,6 +530,39 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return dbo;
}
/**
* Potentially replaces dots in the given map key with the configured map key replacement if configured or aborts
* conversion if none is configured.
*
* @see #setMapKeyDotReplacement(String)
* @param source
* @return
*/
protected String potentiallyEscapeMapKey(String source) {
if (!source.contains(".")) {
return source;
}
if (mapKeyDotReplacement == null) {
throw new MappingException(String.format("Map key %s contains dots but no replacement was configured! Make "
+ "sure map keys don't contain dots in the first place or configure an appropriate replacement!", source));
}
return source.replaceAll("\\.", mapKeyDotReplacement);
}
/**
* Translates the map key replacements in the given key just read with a dot in case a map key replacement has been
* configured.
*
* @param source
* @return
*/
protected String potentiallyUnescapeMapKey(String source) {
return mapKeyDotReplacement == null ? source : source.replaceAll(mapKeyDotReplacement, "\\.");
}
/**
* Adds custom type information to the given {@link DBObject} if necessary. That is if the value is not the same as
* the one given. This is usually the case if you store a subtype of the actual declared type of the property.
@@ -729,12 +775,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
continue;
}
Object key = entry.getKey();
Object key = potentiallyUnescapeMapKey(entry.getKey());
TypeInformation<?> keyTypeInformation = type.getComponentType();
if (keyTypeInformation != null) {
Class<?> keyType = keyTypeInformation.getType();
key = conversionService.convert(entry.getKey(), keyType);
key = conversionService.convert(key, keyType);
}
Object value = entry.getValue();

View File

@@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.mapping.Field;
@@ -958,6 +959,45 @@ public class MappingMongoConverterUnitTests {
assertThat(values, hasItems("1", "2"));
}
/**
* @see DATAMONGO-380
*/
@Test(expected = MappingException.class)
public void rejectsMapWithKeyContainingDotsByDefault() {
converter.write(Collections.singletonMap("foo.bar", "foobar"), new BasicDBObject());
}
/**
* @see DATAMONGO-380
*/
@Test
public void escapesDotInMapKeysIfReplacementConfigured() {
converter.setMapKeyDotReplacement("~");
DBObject dbObject = new BasicDBObject();
converter.write(Collections.singletonMap("foo.bar", "foobar"), dbObject);
assertThat((String) dbObject.get("foo~bar"), is("foobar"));
assertThat(dbObject.containsField("foo.bar"), is(false));
}
/**
* @see DATAMONGO-380
*/
@Test
@SuppressWarnings("unchecked")
public void unescapesDotInMapKeysIfReplacementConfigured() {
converter.setMapKeyDotReplacement("~");
DBObject dbObject = new BasicDBObject("foo~bar", "foobar");
Map<String, String> result = converter.read(Map.class, dbObject);
assertThat(result.get("foo.bar"), is("foobar"));
assertThat(result.containsKey("foobar"), is(false));
}
static class GenericType<T> {
T content;
}