From 88558b67c3461fe5bfe1475d3297cd3908478b18 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 10 Apr 2014 18:36:37 +0200 Subject: [PATCH] DATAMONGO-866 - Polishing for new field naming strategy configuration support. Use the camel case split logic from Spring Data Commons (introduced for DATACMNS-486) in a common CamelCaseSplittingFieldNamingStrategy super class. MappingMongoConverterParser now also rejects the configuration if both abbreviate-field-names and field-naming-strategy-ref are configured. --- .../config/AbstractMongoConfiguration.java | 1 + .../config/MappingMongoConverterParser.java | 40 +++++++--- ...elCaseAbbreviatingFieldNamingStrategy.java | 25 +++--- ...CamelCaseSplittingFieldNamingStrategy.java | 79 +++++++++++++++++++ .../core/mapping/FieldNamingStrategy.java | 2 +- ...aseWithUnderscoresFieldNamingStrategy.java | 62 --------------- .../mapping/SnakeCaseFieldNamingStrategy.java | 34 ++++++++ ...gMongoConverterParserIntegrationTests.java | 20 +++++ ...nakeCaseFieldNamingStrategyUnitTests.java} | 14 ++-- .../resources/namespace/converter-invalid.xml | 10 +++ 10 files changed, 191 insertions(+), 96 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CamelCaseSplittingFieldNamingStrategy.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategy.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategy.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/{LowerCaseWithUnderscoresFieldNamingStrategyUnitTests.java => SnakeCaseFieldNamingStrategyUnitTests.java} (80%) create mode 100644 spring-data-mongodb/src/test/resources/namespace/converter-invalid.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java index b7a6a1fa6..8e18966a4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java @@ -237,6 +237,7 @@ public abstract class AbstractMongoConfiguration { * Configures a {@link FieldNamingStrategy} on the {@link MongoMappingContext} instance created. * * @return + * @since 1.5 */ protected FieldNamingStrategy fieldNamingStrategy() { return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java index ef502f684..b5de837fd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.parsing.ReaderContext; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -209,18 +210,7 @@ public class MappingMongoConverterParser implements BeanDefinitionParser { mappingContextBuilder.addPropertyValue("simpleTypeHolder", simpleTypesDefinition); } - String abbreviateFieldNames = element.getAttribute("abbreviate-field-names"); - - if ("true".equals(abbreviateFieldNames)) { - mappingContextBuilder.addPropertyValue("fieldNamingStrategy", new RootBeanDefinition( - CamelCaseAbbreviatingFieldNamingStrategy.class)); - } - - String fieldNamingStrategy = element.getAttribute("field-naming-strategy-ref"); - - if (StringUtils.hasText(fieldNamingStrategy)) { - mappingContextBuilder.addPropertyValue("fieldNamingStrategy", new RuntimeBeanReference(fieldNamingStrategy)); - } + parseFieldNamingStrategy(element, parserContext.getReaderContext(), mappingContextBuilder); ctxRef = converterId == null || DEFAULT_CONVERTER_BEAN_NAME.equals(converterId) ? MAPPING_CONTEXT_BEAN_NAME : converterId + "." + MAPPING_CONTEXT_BEAN_NAME; @@ -229,6 +219,32 @@ public class MappingMongoConverterParser implements BeanDefinitionParser { return ctxRef; } + private static void parseFieldNamingStrategy(Element element, ReaderContext context, BeanDefinitionBuilder builder) { + + String abbreviateFieldNames = element.getAttribute("abbreviate-field-names"); + String fieldNamingStrategy = element.getAttribute("field-naming-strategy-ref"); + + if (StringUtils.hasText(fieldNamingStrategy) && StringUtils.hasText(abbreviateFieldNames)) { + + context + .error("Only one of the attributes abbreviate-field-names and field-naming-strategy-ref can be configured!", + element); + return; + } + + Object value = null; + + if ("true".equals(abbreviateFieldNames)) { + value = new RootBeanDefinition(CamelCaseAbbreviatingFieldNamingStrategy.class); + } else if (StringUtils.hasText(fieldNamingStrategy)) { + value = new RuntimeBeanReference(fieldNamingStrategy); + } + + if (value != null) { + builder.addPropertyValue("fieldNamingStrategy", value); + } + } + private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { List customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java index 08dfa9cfe..2019cc629 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package org.springframework.data.mongodb.core.mapping; -import java.util.Locale; - /** * {@link FieldNamingStrategy} that abbreviates field names by using the very first letter of the camel case parts of * the {@link MongoPersistentProperty}'s name. @@ -24,23 +22,18 @@ import java.util.Locale; * @since 1.3 * @author Oliver Gierke */ -public class CamelCaseAbbreviatingFieldNamingStrategy implements FieldNamingStrategy { +public class CamelCaseAbbreviatingFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy { - private static final String CAMEL_CASE_PATTERN = "(? parts = ParsingUtils.splitCamelCaseToLower(property.getName()); + List result = new ArrayList(); + + for (String part : parts) { + + String candidate = preparePart(part); + + if (StringUtils.hasText(candidate)) { + result.add(candidate); + } + } + + return StringUtils.collectionToDelimitedString(result, delimiter); + } + + /** + * Callback to prepare the uncapitalized part obtained from the split up of the camel case source. Default + * implementation returns the part as is. + * + * @param part + * @return + */ + protected String preparePart(String part) { + return part; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java index e8ffe3945..c2c2941a8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/FieldNamingStrategy.java @@ -21,7 +21,7 @@ package org.springframework.data.mongodb.core.mapping; * @see DocumentField * @see PropertyNameFieldNamingStrategy * @see CamelCaseAbbreviatingFieldNamingStrategy - * @see LowerCaseWithUnderscoresFieldNamingStrategy + * @see SnakeCaseFieldNamingStrategy * @since 1.3 * @author Oliver Gierke */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategy.java deleted file mode 100644 index 59301d1d4..000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategy.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2013 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.core.mapping; - -/** - * {@link FieldNamingStrategy} that translates typical camel case Java - * property names to lower case JSON element names, separated by - * underscores. - * - * Source: jackson-databind, Apache 2 License - * https://github.com/FasterXML/jackson-databind/blob/2.3/src/main/java/com/fasterxml/jackson/databind/PropertyNamingStrategy.java - * - * @since 1.5 - * @author Ryan Tenney - */ -public class LowerCaseWithUnderscoresFieldNamingStrategy implements FieldNamingStrategy { - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.mapping.FieldNamingStrategy#getFieldName(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty) - */ - public String getFieldName(MongoPersistentProperty property) { - String input = property.getName(); - int length = input.length(); - StringBuilder result = new StringBuilder(length * 2); - int resultLength = 0; - boolean wasPrevTranslated = false; - for (int i = 0; i < length; i++) { - char c = input.charAt(i); - if (i > 0 || c != '_') { // skip first starting underscore - if (Character.isUpperCase(c)) { - if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') { - result.append('_'); - resultLength++; - } - c = Character.toLowerCase(c); - wasPrevTranslated = true; - } - else { - wasPrevTranslated = false; - } - result.append(c); - resultLength++; - } - } - return resultLength > 0 ? result.toString() : input; - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategy.java new file mode 100644 index 000000000..cf5328b31 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategy.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +/** + * {@link FieldNamingStrategy} that translates typical camel case Java property names to lower case JSON element names, + * separated by underscores. + * + * @since 1.5 + * @author Ryan Tenney + * @author Oliver Gierke + */ +public class SnakeCaseFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy { + + /** + * Creates a new {@link SnakeCaseFieldNamingStrategy}. + */ + public SnakeCaseFieldNamingStrategy() { + super("_"); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java index 5a7951f1e..793117688 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/config/MappingMongoConverterParserIntegrationTests.java @@ -22,8 +22,12 @@ import java.util.Collections; import java.util.Set; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.parsing.BeanDefinitionParsingException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.convert.TypeDescriptor; @@ -49,6 +53,7 @@ import com.mongodb.DBObject; public class MappingMongoConverterParserIntegrationTests { DefaultListableBeanFactory factory; + public @Rule ExpectedException exception = ExpectedException.none(); @Before public void setUp() { @@ -104,6 +109,21 @@ public class MappingMongoConverterParserIntegrationTests { assertThat(strategy.getBeanClassName(), is(CamelCaseAbbreviatingFieldNamingStrategy.class.getName())); } + /** + * @see DATAMONGO-866 + */ + @Test + public void rejectsInvalidFieldNamingStrategyConfiguration() { + + exception.expect(BeanDefinitionParsingException.class); + exception.expectMessage("abbreviate-field-names"); + exception.expectMessage("field-naming-strategy-ref"); + + BeanDefinitionRegistry factory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); + reader.loadBeanDefinitions(new ClassPathResource("namespace/converter-invalid.xml")); + } + @Component public static class SampleConverter implements Converter { public DBObject convert(Person source) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java similarity index 80% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategyUnitTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java index 19358bf58..f791f1ffa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/LowerCaseWithUnderscoresFieldNamingStrategyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,19 +25,23 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; /** - * Unit tests for {@link LowerCaseWithUnderscoresFieldNamingStrategy}. + * Unit tests for {@link SnakeCaseFieldNamingStrategy}. * * @author Ryan Tenney + * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) -public class LowerCaseWithUnderscoresFieldNamingStrategyUnitTests { +public class SnakeCaseFieldNamingStrategyUnitTests { - FieldNamingStrategy strategy = new LowerCaseWithUnderscoresFieldNamingStrategy(); + FieldNamingStrategy strategy = new SnakeCaseFieldNamingStrategy(); @Mock MongoPersistentProperty property; + /** + * @see DATAMONGO-866 + */ @Test - public void foo() { + public void rendersSnakeCaseFieldNames() { assertFieldNameForPropertyName("fooBar", "foo_bar"); assertFieldNameForPropertyName("FooBar", "foo_bar"); diff --git a/spring-data-mongodb/src/test/resources/namespace/converter-invalid.xml b/spring-data-mongodb/src/test/resources/namespace/converter-invalid.xml new file mode 100644 index 000000000..7d7204007 --- /dev/null +++ b/spring-data-mongodb/src/test/resources/namespace/converter-invalid.xml @@ -0,0 +1,10 @@ + + + + + +