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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user