Browse Source

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(…).
pull/1/head
Oliver Gierke 14 years ago
parent
commit
e531508bdc
  1. 52
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  2. 40
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

52
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

@ -81,6 +81,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -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;
private SpELContext spELContext;
@ -120,6 +121,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -120,6 +121,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()
@ -507,7 +520,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -507,7 +520,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()) {
@ -528,6 +541,39 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -528,6 +541,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.
@ -692,12 +738,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -692,12 +738,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();

40
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

@ -45,6 +45,7 @@ import org.springframework.beans.factory.annotation.Value; @@ -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 { @@ -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;
}

Loading…
Cancel
Save