From e9ae5ba1c5649c475e95015f29a8d2278dd65b02 Mon Sep 17 00:00:00 2001 From: Michael Nitschinger Date: Mon, 14 Apr 2014 14:30:07 +0200 Subject: [PATCH] DATACOUCH-82 - Implement custom FieldNamingStrategies and abbreviation support. --- .../AbstractCouchbaseConfiguration.java | 23 +++++- .../BasicCouchbasePersistentProperty.java | 22 +++++- ...elCaseAbbreviatingFieldNamingStrategy.java | 38 ++++++++++ ...CamelCaseSplittingFieldNamingStrategy.java | 74 +++++++++++++++++++ .../core/mapping/CouchbaseMappingContext.java | 23 +++++- .../mapping/FallbackFieldNamingStrategy.java | 32 ++++++++ .../core/mapping/FieldNamingStrategy.java | 39 ++++++++++ .../mapping/SnakeCaseFieldNamingStrategy.java | 35 +++++++++ ...BasicCouchbasePersistentPropertyTests.java | 2 +- ...reviatingFieldNamingStrategyUnitTests.java | 53 +++++++++++++ ...SnakeCaseFieldNamingStrategyUnitTests.java | 55 ++++++++++++++ 11 files changed, 389 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseSplittingFieldNamingStrategy.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/FallbackFieldNamingStrategy.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/FieldNamingStrategy.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategy.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java create mode 100644 src/test/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index e8a1b633..ec9bbde0 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -29,8 +29,7 @@ import org.springframework.data.couchbase.core.convert.CustomConversions; import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter; import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService; import org.springframework.data.couchbase.core.convert.translation.TranslationService; -import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext; -import org.springframework.data.couchbase.core.mapping.Document; +import org.springframework.data.couchbase.core.mapping.*; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -147,9 +146,12 @@ public abstract class AbstractCouchbaseConfiguration { CouchbaseMappingContext mappingContext = new CouchbaseMappingContext(); mappingContext.setInitialEntitySet(getInitialEntitySet()); mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder()); + mappingContext.setFieldNamingStrategy(fieldNamingStrategy()); return mappingContext; } + + /** * Register custom Converters in a {@link CustomConversions} object if required. These * {@link CustomConversions} will be registered with the {@link #mappingCouchbaseConverter()} and @@ -197,6 +199,23 @@ public abstract class AbstractCouchbaseConfiguration { return getClass().getPackage().getName(); } + /** + * Set to true if field names should be abbreviated with the {@link org.springframework.data.couchbase.core.mapping.CamelCaseAbbreviatingFieldNamingStrategy}. + * + * @return true if field names should be abbreviated, default is false. + */ + protected boolean abbreviateFieldNames() { + return false; + } + + /** + * Configures a {@link FieldNamingStrategy} on the {@link CouchbaseMappingContext} instance created. + * + * @return the naming strategy. + */ + protected FieldNamingStrategy fieldNamingStrategy() { + return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy() : FallbackFieldNamingStrategy.INSTANCE; + } /** * Converts the given list of hostnames into parsable URIs. diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java index a7d3b3f1..ea944aae 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentProperty.java @@ -18,6 +18,7 @@ package org.springframework.data.couchbase.core.mapping; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.model.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.util.StringUtils; @@ -36,6 +37,8 @@ public class BasicCouchbasePersistentProperty extends AnnotationBasedPersistentProperty implements CouchbasePersistentProperty { + private final FieldNamingStrategy fieldNamingStrategy; + /** * Create a new instance of the BasicCouchbasePersistentProperty class. * @@ -45,8 +48,11 @@ public class BasicCouchbasePersistentProperty * @param simpleTypeHolder the type holder. */ public BasicCouchbasePersistentProperty(final Field field, final PropertyDescriptor propertyDescriptor, - final CouchbasePersistentEntity owner, final SimpleTypeHolder simpleTypeHolder) { + final CouchbasePersistentEntity owner, final SimpleTypeHolder simpleTypeHolder, + final FieldNamingStrategy fieldNamingStrategy) { super(field, propertyDescriptor, owner, simpleTypeHolder); + this.fieldNamingStrategy = fieldNamingStrategy == null ? FallbackFieldNamingStrategy.INSTANCE + : fieldNamingStrategy; } /** @@ -68,8 +74,18 @@ public class BasicCouchbasePersistentProperty org.springframework.data.couchbase.core.mapping.Field annotation = getField(). getAnnotation(org.springframework.data.couchbase.core.mapping.Field.class); - return annotation != null && StringUtils.hasText(annotation.value()) - ? annotation.value() : field.getName(); + if (annotation != null && StringUtils.hasText(annotation.value())) { + return annotation.value(); + } + + String fieldName = fieldNamingStrategy.getFieldName(this); + + if (!StringUtils.hasText(fieldName)) { + throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s!", + this, fieldNamingStrategy.getClass())); + } + + return fieldName; } } diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java new file mode 100644 index 00000000..861472cf --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategy.java @@ -0,0 +1,38 @@ +/* + * Copyright 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.couchbase.core.mapping; + +/** + * {@link FieldNamingStrategy} that abbreviates field names by using the very first letter of the camel case parts of + * the {@link CouchbasePersistentProperty}'s name. + * + * @since 1.1 + * @author Michael Nitschinger + * @author Oliver Gierke + */ +public class CamelCaseAbbreviatingFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy { + + public CamelCaseAbbreviatingFieldNamingStrategy() { + super(""); + } + + @Override + protected String preparePart(String part) { + return part.substring(0, 1); + } + + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseSplittingFieldNamingStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseSplittingFieldNamingStrategy.java new file mode 100644 index 00000000..1a96f569 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CamelCaseSplittingFieldNamingStrategy.java @@ -0,0 +1,74 @@ +/* + * Copyright 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.couchbase.core.mapping; + +import org.springframework.data.util.ParsingUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Configurable {@link FieldNamingStrategy} that splits up camel-case property names and reconcatenates them using a + * configured delimiter. Individual parts of the name can be manipulated using {@link #preparePart(String)}. + * + * @author Oliver Gierke + * @author Michael Nitschinger + * @since 1.1 + */ +public class CamelCaseSplittingFieldNamingStrategy implements FieldNamingStrategy { + + private final String delimiter; + + /** + * Creates a new {@link CamelCaseSplittingFieldNamingStrategy}. + * + * @param delimiter must not be {@literal null}. + */ + public CamelCaseSplittingFieldNamingStrategy(String delimiter) { + Assert.notNull(delimiter, "Delimiter must not be null!"); + this.delimiter = delimiter; + } + + @Override + public String getFieldName(final CouchbasePersistentProperty property) { + List 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 the part. + * @return the prepared part. + */ + protected String preparePart(String part) { + return part; + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java index 4aa4beb4..d82414b7 100644 --- a/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/CouchbaseMappingContext.java @@ -41,6 +41,27 @@ public class CouchbaseMappingContext */ private ApplicationContext context; + /** + * The default field naming strategy. + */ + private static final FieldNamingStrategy DEFAULT_NAMING_STRATEGY = FallbackFieldNamingStrategy.INSTANCE; + + /** + * The field naming strategy to use. + */ + private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY; + + /** + * Configures the {@link FieldNamingStrategy} to be used to determine the field name if no manual mapping is applied. + * Defaults to a strategy using the plain property name. + * + * @param fieldNamingStrategy the {@link FieldNamingStrategy} to be used to determine the field name if no manual + * mapping is applied. + */ + public void setFieldNamingStrategy(final FieldNamingStrategy fieldNamingStrategy) { + this.fieldNamingStrategy = fieldNamingStrategy == null ? DEFAULT_NAMING_STRATEGY : fieldNamingStrategy; + } + /** * Creates a concrete entity based out of the type information passed. * @@ -69,7 +90,7 @@ public class CouchbaseMappingContext @Override protected CouchbasePersistentProperty createPersistentProperty(final Field field, final PropertyDescriptor descriptor, final BasicCouchbasePersistentEntity owner, final SimpleTypeHolder simpleTypeHolder) { - return new BasicCouchbasePersistentProperty(field, descriptor, owner, simpleTypeHolder); + return new BasicCouchbasePersistentProperty(field, descriptor, owner, simpleTypeHolder, fieldNamingStrategy); } /** diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/FallbackFieldNamingStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/FallbackFieldNamingStrategy.java new file mode 100644 index 00000000..94b087c8 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/FallbackFieldNamingStrategy.java @@ -0,0 +1,32 @@ +/* + * Copyright 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.couchbase.core.mapping; + +/** + * {@link FieldNamingStrategy} simply using the {@link CouchbasePersistentProperty}'s name. + * + * @author Michael Nitschinger + * @since 1.1 + */ +public enum FallbackFieldNamingStrategy implements FieldNamingStrategy { + INSTANCE; + + @Override + public String getFieldName(final CouchbasePersistentProperty property) { + return property.getName(); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/FieldNamingStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/FieldNamingStrategy.java new file mode 100644 index 00000000..8282e19a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/FieldNamingStrategy.java @@ -0,0 +1,39 @@ +/* + * Copyright 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.couchbase.core.mapping; + +/** + * SPI interface to determine how to name document fields in cases the field name is not manually defined. + * + * @see FallbackFieldNamingStrategy + * @see CamelCaseAbbreviatingFieldNamingStrategy + * @see SnakeCaseFieldNamingStrategy + * + * @author Michael Nitschinger + * @author Oliver Gierke + * @since 1.1 + */ +public interface FieldNamingStrategy { + + /** + * Returns the field name to be used for the given {@link CouchbasePersistentProperty}. + * + * @param property must not be {@literal null} or empty. + * @return the field name to be used. + */ + String getFieldName(CouchbasePersistentProperty property); + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategy.java b/src/main/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategy.java new file mode 100644 index 00000000..4040de95 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategy.java @@ -0,0 +1,35 @@ +/* + * Copyright 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.couchbase.core.mapping; + +/** + * {@link FieldNamingStrategy} that translates typical camel case Java property names to lower case JSON element names, + * separated by underscores. + * + * @since 1.1 + * @author Ryan Tenney + * @author Michael Nitschinger + */ +public class SnakeCaseFieldNamingStrategy extends CamelCaseSplittingFieldNamingStrategy { + + /** + * Creates a new {@link SnakeCaseFieldNamingStrategy}. + */ + public SnakeCaseFieldNamingStrategy() { + super("_"); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java index 1a5e3c37..9ede60d3 100644 --- a/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/BasicCouchbasePersistentPropertyTests.java @@ -74,7 +74,7 @@ public class BasicCouchbasePersistentPropertyTests { */ private CouchbasePersistentProperty getPropertyFor(Field field) { return new BasicCouchbasePersistentProperty(field, null, entity, - new SimpleTypeHolder()); + new SimpleTypeHolder(), FallbackFieldNamingStrategy.INSTANCE); } /** diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java new file mode 100644 index 00000000..d53fc139 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.couchbase.core.mapping; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link CamelCaseAbbreviatingFieldNamingStrategy}. + * + * @author Oliver Gierke + * @author Michael Nitschinger + * @since 1.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class CamelCaseAbbreviatingFieldNamingStrategyUnitTests { + + FieldNamingStrategy strategy = new CamelCaseAbbreviatingFieldNamingStrategy(); + + @Mock + CouchbasePersistentProperty property; + + @Test + public void shouldConvertPropertyNames() { + assertFieldNameForPropertyName("fooBar", "fb"); + assertFieldNameForPropertyName("fooBARFooBar", "fbfb"); + } + + private void assertFieldNameForPropertyName(final String propertyName, final String fieldName) { + when(property.getName()).thenReturn(propertyName); + assertThat(strategy.getFieldName(property), is(fieldName)); + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java new file mode 100644 index 00000000..f8b28692 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/core/mapping/SnakeCaseFieldNamingStrategyUnitTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 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.couchbase.core.mapping; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link SnakeCaseFieldNamingStrategy}. + * + * @author Ryan Tenney + * @author Michael Nitschinger + * @since 1.1 + */ +@RunWith(MockitoJUnitRunner.class) +public class SnakeCaseFieldNamingStrategyUnitTests { + + FieldNamingStrategy strategy = new SnakeCaseFieldNamingStrategy(); + + @Mock + CouchbasePersistentProperty property; + + @Test + public void shouldConvertPropertyNames() { + assertFieldNameForPropertyName("fooBar", "foo_bar"); + assertFieldNameForPropertyName("FooBar", "foo_bar"); + assertFieldNameForPropertyName("foo_bar", "foo_bar"); + assertFieldNameForPropertyName("FOO_BAR", "foo_bar"); + } + + private void assertFieldNameForPropertyName(final String propertyName, final String fieldName) { + when(property.getName()).thenReturn(propertyName); + assertThat(strategy.getFieldName(property), is(fieldName)); + } + +}