diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 9a63a5a45..ef2a003a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/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 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 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 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 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 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(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 6698a5daf..ec4fe5764 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/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; 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 result = converter.read(Map.class, dbObject); + + assertThat(result.get("foo.bar"), is("foobar")); + assertThat(result.containsKey("foobar"), is(false)); + } + static class GenericType { T content; }