From 06b6b266dbf224dcc6b0a548ee556276c08d09f3 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 20 Aug 2015 16:16:07 +0200 Subject: [PATCH] DATAREST-654 - Added suport for internationalization of enum values. RepositoryRestConfiguration now exposes an option to enable enum value serialization and a nested configuration object to tweak the details. If enabled, a Jackson serializer and deserializer is registered trying to resolve the enum values from the Spring Data REST resource bundle using the fully-qualified enum value name as key. If no explicitly configured value is configured a default translation is triggered that capitalizes the lowercased value name replacing the underscores with spaces (e.g. PAYMENT_EXPECTED -> Payment expected). This can be opted out of, of course. On the parsing side the deserializer will also consult the resourcebundle and default translation but also accepting the enum name as is (also opt-outable). Deprecated non-bean-style accessors for projection and metadata configuration on RepositoryRestConfiguration to make these options tweakable via Spring Boot application properties by default. --- .../config/EnumTranslationConfiguration.java | 44 +++++ .../config/RepositoryRestConfiguration.java | 64 +++++- .../RepositoryRestConfigurationUnitTests.java | 24 ++- .../data/rest/core/RepositoryTestsConfig.java | 8 +- .../halbrowser/HalBrowserUnitTests.java | 11 +- .../data/rest/webmvc/alps/AlpsController.java | 2 +- ...eInformationToAlpsDescriptorConverter.java | 26 +-- .../RepositoryRestMvcConfiguration.java | 21 +- .../data/rest/webmvc/json/EnumTranslator.java | 180 +++++++++++++++++ .../rest/webmvc/json/JacksonSerializers.java | 154 +++++++++++++++ .../data/rest/webmvc/json/JsonSchema.java | 40 +++- ...PersistentEntityToJsonSchemaConverter.java | 7 +- .../webmvc/support/RepositoryEntityLinks.java | 7 +- .../AugmentingHandlerMappingUnitTests.java | 10 +- ...RepositoryRestHandlerMappingUnitTests.java | 6 +- .../webmvc/json/EnumTranslatorUnitTests.java | 185 ++++++++++++++++++ .../webmvc/json/JacksonMetadataUnitTests.java | 62 ++++-- ...rsistentEntityJackson2ModuleUnitTests.java | 7 +- ...tEntityToJsonSchemaConverterUnitTests.java | 4 +- .../webmvc/json/RepositoryTestsConfig.java | 6 +- ...RepositoryEntityLinksIntegrationTests.java | 2 +- 21 files changed, 799 insertions(+), 71 deletions(-) create mode 100644 spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EnumTranslationConfiguration.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/EnumTranslator.java create mode 100644 spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonSerializers.java create mode 100644 spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/EnumTranslatorUnitTests.java diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EnumTranslationConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EnumTranslationConfiguration.java new file mode 100644 index 000000000..8de705ac0 --- /dev/null +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/EnumTranslationConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 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.rest.core.config; + +/** + * Configuration options for enum value translation. + * + * @author Oliver Gierke + * @since 2.4 + * @soundtrack Wallis Bird - Measuring Cities (Yeah! Wallis Bird live 2007-2014) + */ +public interface EnumTranslationConfiguration { + + /** + * Configures whether the default translation of enum names shall be applied. Defaults to {@literal true}. This means + * the configuration will turn enum names into human friendly {@link String}s and also parse them if - only if - no + * explicit translation is available. + * + * @param enableDefaultTranslation whether to enable the default translation of enum names. + */ + void setEnableDefaultTranslation(boolean enableDefaultTranslation); + + /** + * Configures whether to always accept the raw enum name when parsing. This is useful if clients were used to send the + * Java enum names shall not be broken even if on the serialization side enum translation is activated. + * + * @param parseEnumNameAsFallback whether to parse the raw enum value as fallback, even if an explicit translation is + * available. + */ + void setParseEnumNameAsFallback(boolean parseEnumNameAsFallback); +} diff --git a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java index 09b1523fa..e892eb35c 100644 --- a/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java +++ b/spring-data-rest-core/src/main/java/org/springframework/data/rest/core/config/RepositoryRestConfiguration.java @@ -55,12 +55,8 @@ public class RepositoryRestConfiguration { private final ProjectionDefinitionConfiguration projectionConfiguration; private final MetadataConfiguration metadataConfiguration; - /** - * Creates a new default {@link RepositoryRestConfiguration}. - */ - public RepositoryRestConfiguration() { - this(new ProjectionDefinitionConfiguration(), new MetadataConfiguration()); - } + private final EnumTranslationConfiguration enumSerializationConfiguration; + private boolean enableEnumTranslation = false; /** * Creates a new {@link RepositoryRestConfiguration} with the given {@link ProjectionDefinitionConfiguration}. @@ -69,13 +65,15 @@ public class RepositoryRestConfiguration { * @param metadataConfiguration must not be {@literal null}. */ public RepositoryRestConfiguration(ProjectionDefinitionConfiguration projectionConfiguration, - MetadataConfiguration metadataConfiguration) { + MetadataConfiguration metadataConfiguration, EnumTranslationConfiguration enumTranslationConfiguration) { Assert.notNull(projectionConfiguration, "ProjectionDefinitionConfiguration must not be null!"); Assert.notNull(metadataConfiguration, "MetadataConfiguration must not be null!"); + Assert.notNull(enumTranslationConfiguration, " must not be null!"); this.projectionConfiguration = projectionConfiguration; this.metadataConfiguration = metadataConfiguration; + this.enumSerializationConfiguration = enumTranslationConfiguration; } /** @@ -449,8 +447,19 @@ public class RepositoryRestConfiguration { * Returns the {@link ProjectionDefinitionConfiguration} to register addition projections. * * @return + * @deprecated since 2.4, use {@link #getProjectionConfiguration()} instead. */ + @Deprecated public ProjectionDefinitionConfiguration projectionConfiguration() { + return getProjectionConfiguration(); + } + + /** + * Returns the {@link ProjectionDefinitionConfiguration} to register addition projections. + * + * @return + */ + public ProjectionDefinitionConfiguration getProjectionConfiguration() { return projectionConfiguration; } @@ -458,8 +467,49 @@ public class RepositoryRestConfiguration { * Returns the {@link MetadataConfiguration} to customize metadata exposure. * * @return + * @deprecated since 2.4, use {@link #getMetadataConfiguration()} instead. */ + @Deprecated public MetadataConfiguration metadataConfiguration() { return metadataConfiguration; } + + /** + * Returns the {@link MetadataConfiguration} to customize metadata exposure. + * + * @return + */ + public MetadataConfiguration getMetadataConfiguration() { + return metadataConfiguration; + } + + /** + * Configures whether to enable enum value translation via the Spring Data REST default resource bundle. Defaults to + * {@literal false} for backwards compatibility reasons. Will use the fully qualified enum name as key. For further + * details see {@link EnumTranslator}. + * + * @param enableEnumTranslation + * @see #getEnumSerializationConfiguration() + */ + public void setEnableEnumTranslation(boolean enableEnumTranslation) { + this.enableEnumTranslation = enableEnumTranslation; + } + + /** + * Returns whether enum value translation is enabled. + * + * @return + */ + public boolean isEnableEnumTranslation() { + return this.enableEnumTranslation; + } + + /** + * Returns the {@link EnumTranslator} for + * + * @return + */ + public EnumTranslationConfiguration getEnumSerializationConfiguration() { + return this.enumSerializationConfiguration; + } } diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationUnitTests.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationUnitTests.java index 89830ebe6..8c83a4e5b 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationUnitTests.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryRestConfigurationUnitTests.java @@ -17,8 +17,13 @@ package org.springframework.data.rest.core; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import org.junit.Before; import org.junit.Test; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.http.MediaType; @@ -30,14 +35,21 @@ import org.springframework.http.MediaType; */ public class RepositoryRestConfigurationUnitTests { + RepositoryRestConfiguration configuration; + + @Before + public void setUp() { + + this.configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); + } + /** * @see DATAREST-34 */ @Test public void returnsBodiesIfAcceptHeaderPresentByDefault() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); - assertThat(configuration.returnBodyOnCreate(MediaType.APPLICATION_JSON_VALUE), is(true)); assertThat(configuration.returnBodyOnUpdate(MediaType.APPLICATION_JSON_VALUE), is(true)); } @@ -48,8 +60,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void doesNotReturnBodiesIfNoAcceptHeaderPresentByDefault() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); - assertThat(configuration.returnBodyOnCreate(null), is(false)); assertThat(configuration.returnBodyOnUpdate(null), is(false)); } @@ -60,8 +70,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void doesNotReturnBodiesIfEmptyAcceptHeaderPresentByDefault() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); - assertThat(configuration.returnBodyOnCreate(""), is(false)); assertThat(configuration.returnBodyOnUpdate(""), is(false)); } @@ -72,7 +80,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void doesNotReturnBodyForUpdateIfExplicitlyDeactivated() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); configuration.setReturnBodyOnUpdate(false); assertThat(configuration.returnBodyOnUpdate(null), is(false)); @@ -86,7 +93,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void doesNotReturnBodyForCreateIfExplicitlyDeactivated() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); configuration.setReturnBodyOnCreate(false); assertThat(configuration.returnBodyOnCreate(null), is(false)); @@ -100,7 +106,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void returnsBodyForUpdateIfExplicitlyActivated() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); configuration.setReturnBodyOnUpdate(true); assertThat(configuration.returnBodyOnUpdate(null), is(true)); @@ -114,7 +119,6 @@ public class RepositoryRestConfigurationUnitTests { @Test public void returnsBodyForCreateIfExplicitlyActivated() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); configuration.setReturnBodyOnCreate(true); assertThat(configuration.returnBodyOnCreate(null), is(true)); diff --git a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java index 6874732fa..ee57576a7 100644 --- a/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java +++ b/spring-data-rest-core/src/test/java/org/springframework/data/rest/core/RepositoryTestsConfig.java @@ -15,6 +15,8 @@ */ package org.springframework.data.rest.core; +import static org.mockito.Mockito.*; + import java.util.Collections; import java.util.List; @@ -26,6 +28,9 @@ import org.springframework.context.annotation.Import; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.Repositories; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.domain.jpa.ConfiguredPersonRepository; import org.springframework.data.rest.core.domain.jpa.JpaRepositoryConfig; @@ -52,7 +57,8 @@ public class RepositoryTestsConfig { @SuppressWarnings("deprecation") @Bean public RepositoryRestConfiguration config() { - RepositoryRestConfiguration config = new RepositoryRestConfiguration(); + RepositoryRestConfiguration config = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); config.setResourceMappingForDomainType(Person.class).setRel("person"); diff --git a/spring-data-rest-hal-browser/src/test/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserUnitTests.java b/spring-data-rest-hal-browser/src/test/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserUnitTests.java index ee5eb32a4..33566955c 100644 --- a/spring-data-rest-hal-browser/src/test/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserUnitTests.java +++ b/spring-data-rest-hal-browser/src/test/java/org/springframework/data/rest/webmvc/halbrowser/HalBrowserUnitTests.java @@ -17,10 +17,15 @@ package org.springframework.data.rest.webmvc.halbrowser; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Collections; +import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.http.HttpHeaders; import org.springframework.mock.web.MockHttpServletRequest; @@ -43,7 +48,9 @@ public class HalBrowserUnitTests { @Test public void createsContextRelativeRedirectForBrowser() throws Exception { - View view = new HalBrowser(new RepositoryRestConfiguration()).browser(); + RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); + View view = new HalBrowser(configuration).browser(); assertThat(view, is(instanceOf(RedirectView.class))); @@ -53,6 +60,6 @@ public class HalBrowserUnitTests { ((AbstractView) view).render(Collections. emptyMap(), request, response); - assertThat(response.getHeader(HttpHeaders.LOCATION), startsWith("/context")); + assertThat(response.getHeader(HttpHeaders.LOCATION), Matchers.startsWith("/context")); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java index aec0710dc..a15db7e01 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/AlpsController.java @@ -96,7 +96,7 @@ public class AlpsController { private void verifyAlpsEnabled() { - if (!configuration.metadataConfiguration().alpsEnabled()) { + if (!configuration.getMetadataConfiguration().alpsEnabled()) { throw new ResourceNotFoundException(); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java index df724a29b..151c2aed7 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/alps/RootResourceInformationToAlpsDescriptorConverter.java @@ -184,7 +184,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { */ private Descriptor buildProjectionDescriptor(ResourceMetadata metadata) { - ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); String projectionParameterName = projectionConfiguration.getParameterName(); Map> projections = projectionConfiguration.getProjectionsFor(metadata.getDomainType()); @@ -221,10 +221,10 @@ public class RootResourceInformationToAlpsDescriptorConverter { AnnotatedMethod getter = definition.getGetter(); Description description = getter.getAnnotation(Description.class); - ResourceDescription fallback = SimpleResourceDescription.defaultFor(String.format("%s.%s", name, - definition.getName())); - ResourceDescription resourceDescription = description == null ? null : new AnnotationBasedResourceDescription( - description, fallback); + ResourceDescription fallback = SimpleResourceDescription + .defaultFor(String.format("%s.%s", name, definition.getName())); + ResourceDescription resourceDescription = description == null ? null + : new AnnotationBasedResourceDescription(description, fallback); descriptors.add(// descriptor().// @@ -259,10 +259,11 @@ public class RootResourceInformationToAlpsDescriptorConverter { return Collections.emptyList(); } - ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); - return projectionConfiguration.hasProjectionFor(type) ? Arrays.asList(buildProjectionDescriptor(mappings - .getMetadataFor(type))) : Collections. emptyList(); + return projectionConfiguration.hasProjectionFor(type) + ? Arrays.asList(buildProjectionDescriptor(mappings.getMetadataFor(type))) + : Collections. emptyList(); } /** @@ -283,7 +284,7 @@ public class RootResourceInformationToAlpsDescriptorConverter { List variables = linkToCollectionResource.getVariables(); List descriptors = new ArrayList(variables.size()); - ProjectionDefinitionConfiguration projectionConfiguration = configuration.projectionConfiguration(); + ProjectionDefinitionConfiguration projectionConfiguration = configuration.getProjectionConfiguration(); for (TemplateVariable variable : variables) { @@ -355,8 +356,8 @@ public class RootResourceInformationToAlpsDescriptorConverter { ResourceMetadata targetTypeMetadata = mappings.getMetadataFor(property.getActualType()); - String href = ProfileController.getPath(configuration, targetTypeMetadata) + - "#" + getRepresentationDescriptorId(targetTypeMetadata); + String href = ProfileController.getPath(configuration, targetTypeMetadata) + "#" + + getRepresentationDescriptorId(targetTypeMetadata); Link link = new Link(href).withSelfRel(); @@ -419,7 +420,8 @@ public class RootResourceInformationToAlpsDescriptorConverter { try { return messageSource.getMessage(description); } catch (NoSuchMessageException o_O) { - return configuration.metadataConfiguration().omitUnresolvableDescriptionKeys() ? null : description.getMessage(); + return configuration.getMetadataConfiguration().omitUnresolvableDescriptionKeys() ? null + : description.getMessage(); } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index a63f01907..7cf4330bd 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -84,7 +84,9 @@ import org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter; import org.springframework.data.rest.webmvc.alps.RootResourceInformationToAlpsDescriptorConverter; import org.springframework.data.rest.webmvc.convert.UriListHttpMessageConverter; import org.springframework.data.rest.webmvc.json.DomainObjectReader; +import org.springframework.data.rest.webmvc.json.EnumTranslator; import org.springframework.data.rest.webmvc.json.Jackson2DatatypeHelper; +import org.springframework.data.rest.webmvc.json.JacksonSerializers; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module; import org.springframework.data.rest.webmvc.json.PersistentEntityToJsonSchemaConverter; import org.springframework.data.rest.webmvc.spi.BackendIdConverter; @@ -249,7 +251,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon configuration.addProjection(projection); } - RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration, metadataConfiguration()); + RepositoryRestConfiguration config = new RepositoryRestConfiguration(configuration, metadataConfiguration(), + enumTranslator()); configurerDelegate.configureRepositoryRestConfiguration(config); configureRepositoryRestConfiguration(config); @@ -528,7 +531,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon handlerAdapter.setWebBindingInitializer(initializer); handlerAdapter.setMessageConverters(defaultMessageConverters()); - if (config().metadataConfiguration().alpsEnabled()) { + if (config().getMetadataConfiguration().alpsEnabled()) { handlerAdapter.setResponseBodyAdvice(Arrays.> asList(alpsJsonHttpMessageConverter())); } @@ -623,7 +626,7 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon List> messageConverters = new ArrayList>(); - if (config().metadataConfiguration().alpsEnabled()) { + if (config().getMetadataConfiguration().alpsEnabled()) { messageConverters.add(alpsJsonHttpMessageConverter()); } @@ -705,7 +708,8 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon projectionFactory.setResourceLoader(applicationContext); PersistentEntityResourceAssemblerArgumentResolver peraResolver = new PersistentEntityResourceAssemblerArgumentResolver( - persistentEntities(), entityLinks(), config().projectionConfiguration(), projectionFactory, resourceMappings()); + persistentEntities(), entityLinks(), config().getProjectionConfiguration(), projectionFactory, + resourceMappings()); HateoasPageableHandlerMethodArgumentResolver pageableResolver = pageableResolver(); HandlerMethodArgumentResolver defaultedPageableResolver = new DefaultedPageableHandlerMethodArgumentResolver( @@ -728,6 +732,10 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.registerModule(geoModule); + if (config().isEnableEnumTranslation()) { + objectMapper.registerModule(new JacksonSerializers(enumTranslator())); + } + Jackson2DatatypeHelper.configureObjectMapper(objectMapper); // Configure custom Modules configurerDelegate.configureJacksonObjectMapper(objectMapper); @@ -736,6 +744,11 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon return objectMapper; } + @Bean + public EnumTranslator enumTranslator() { + return new EnumTranslator(resourceDescriptionMessageSourceAccessor()); + } + @SuppressWarnings("unchecked") private Set> getProjections(Repositories repositories) { diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/EnumTranslator.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/EnumTranslator.java new file mode 100644 index 000000000..8a68658ff --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/EnumTranslator.java @@ -0,0 +1,180 @@ +/* + * Copyright 2015 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.rest.webmvc.json; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.springframework.context.NoSuchMessageException; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Configuration to tweak enum serialization. + * + * @author Oliver Gierke + */ +public class EnumTranslator implements EnumTranslationConfiguration { + + private final MessageSourceAccessor messageSourceAccessor; + + private boolean enableDefaultTranslation; + private boolean parseEnumNameAsFallback; + + /** + * Creates a new {@link EnumTranslator} using the given {@link MessageSourceAccessor}. + * + * @param messageSourceAccessor must not be {@literal null}. + */ + public EnumTranslator(MessageSourceAccessor messageSourceAccessor) { + + Assert.notNull(messageSourceAccessor, "MessageSourceAccessor must not be null!"); + + this.messageSourceAccessor = messageSourceAccessor; + this.enableDefaultTranslation = true; + this.parseEnumNameAsFallback = true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EnumTranslationConfiguration#setEnableDefaultTranslation(boolean) + */ + @Override + public void setEnableDefaultTranslation(boolean enableDefaultTranslation) { + this.enableDefaultTranslation = enableDefaultTranslation; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.core.config.EnumTranslationConfiguration#setParseEnumNameAsFallback(boolean) + */ + @Override + public void setParseEnumNameAsFallback(boolean parseEnumNameAsFallback) { + this.parseEnumNameAsFallback = parseEnumNameAsFallback; + } + + /** + * Resolves the given enum value into a {@link String} consulting the configured {@link MessageSourceAccessor} + * potentially falling back to the default translation if configured. Returning the plain enum name if no resolution + * applies. + * + * @param value must not be {@literal null}. + * @return + */ + public String asText(Enum value) { + + Assert.notNull(value, "Enum value must not be null!"); + + String code = String.format("%s.%s", value.getDeclaringClass().getName(), value.name()); + + try { + return messageSourceAccessor.getMessage(code); + } catch (NoSuchMessageException o_O) { + return enableDefaultTranslation ? toDefault(value) : value.name(); + } + } + + public List getValues(Class> type) { + + List result = new ArrayList(); + + for (Enum value : type.getEnumConstants()) { + result.add(asText(value)); + } + + return result; + } + + /** + * Parses the given source text into the corresponding enum value using the configured {@link MessageSourceAccessor} + * potentially falling back to the default translation or the plain enum name if configured. + * + * @param type must not be {@literal null}. + * @param text can be {@literal null} + * @return the resolved enum or {@literal null} if the resolution failed. + */ + public > T fromText(Class type, String text) { + + if (!StringUtils.hasText(text)) { + return null; + } + + Assert.notNull(type, "Enum type must not be null!"); + + T value = resolveEnum(type, text, true); + + if (value != null) { + return value; + } + + value = fromDefault(type, text); + + // Only parse default translation if no explicit translation is available + if (value != null && enableDefaultTranslation && asText(value).equals(text)) { + return value; + } + + return parseEnumNameAsFallback ? resolveEnum(type, text, false) : null; + } + + /** + * Resolves the given {@link String} text into an enum value of the given type potentially trying to resolve it + * through the configured {@link MessageSourceAccessor}. + * + * @param type must not be {@literal null}. + * @param text must not be {@literal null} or empty. + * @param resolve whether to resolve the source {@link String} through the message source. + * @return + */ + @SuppressWarnings("unchecked") + private > T resolveEnum(Class type, String text, boolean resolve) { + + for (Enum value : type.getEnumConstants()) { + + String resolved = resolve ? asText(value) : value.name(); + + if (resolved != null && resolved.equals(text)) { + return (T) value; + } + } + + return null; + } + + /** + * Renders a default translation for the given enum (capitalized, lower case, underscores replaced by spaces). + * + * @param value must not be {@literal null}. + * @return + */ + private String toDefault(Enum value) { + return StringUtils.capitalize(value.name().toLowerCase(Locale.US).replaceAll("_", " ")); + } + + /** + * Tries to obtain an enum value assuming the given text is a default translation of the enum name. + * + * @param type must not be {@literal null}. + * @param text must not be {@literal null} or empty. + * @return + */ + private > T fromDefault(Class type, String text) { + return resolveEnum(type, text.toUpperCase(Locale.US).replaceAll(" ", "_"), true); + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonSerializers.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonSerializers.java new file mode 100644 index 000000000..26a9f5ee6 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JacksonSerializers.java @@ -0,0 +1,154 @@ +/* + * Copyright 2015 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.rest.webmvc.json; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.rest.webmvc.json.JsonSchema.EnumProperty; +import org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +/** + * Custom Spring Data REST Jackson serializers. + * + * @author Oliver Gierke + * @since 2.4 + * @soundtrack Wallis Bird - I Could Be Your Man (Yeah! Wallis Bird Live 2007-2014) + */ +public class JacksonSerializers extends SimpleModule { + + private static final long serialVersionUID = 4396776390917947147L; + + /** + * Creates a new {@link JacksonSerializers} with the given {@link EnumTranslator}. + * + * @param translator must not be {@literal null}. + */ + public JacksonSerializers(EnumTranslator translator) { + + Assert.notNull(translator, "EnumTranslator must not be null!"); + + SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(Enum.class, new EnumTranslatingSerializer(translator)); + setSerializers(serializers); + + SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(Enum.class, new EnumTranslatingDeserializer(translator)); + setDeserializers(deserializers); + } + + /** + * An enum serializer to translate raw enum values into values resolved through a resource bundle. + * + * @author Oliver Gierke + */ + @SuppressWarnings("rawtypes") + public static class EnumTranslatingSerializer extends StdSerializerimplements JsonSchemaPropertyCustomizer { + + private static final long serialVersionUID = -6706924011396258646L; + + private final EnumTranslator translator; + + /** + * Creates a new {@link EnumTranslatingSerializer} using the given {@link EnumTranslator}. + * + * @param translator must not be {@literal null}. + */ + public EnumTranslatingSerializer(EnumTranslator translator) { + + super(Enum.class); + + Assert.notNull(translator, "EnumTranslator must not be null!"); + + this.translator = translator; + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) + */ + @Override + public void serialize(Enum value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(translator.asText(value)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.rest.webmvc.json.JsonSchemaPropertyCustomizer#customize(org.springframework.data.rest.webmvc.json.JsonSchema.JsonSchemaProperty, org.springframework.data.util.TypeInformation) + */ + @Override + public JsonSchemaProperty customize(JsonSchemaProperty property, TypeInformation type) { + + List values = new ArrayList(); + + for (Object value : type.getType().getEnumConstants()) { + values.add(translator.asText((Enum) value)); + } + + return ((EnumProperty) property).withValues(values); + } + } + + /** + * Enum deserializer that uses a resource bundle to resolve enum values. + * + * @author Oliver Gierke + */ + @SuppressWarnings("rawtypes") + public static class EnumTranslatingDeserializer extends StdDeserializer { + + private static final long serialVersionUID = 5305284644923180079L; + + private final EnumTranslator translator; + + /** + * Creates a new {@link EnumTranslatingDeserializer} using the given {@link EnumTranslator}. + * + * @param translator must not be {@literal null}. + */ + public EnumTranslatingDeserializer(EnumTranslator translator) { + + super(Enum.class); + + Assert.notNull(translator, "EnumTranslator must not be null!"); + this.translator = translator; + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) + */ + @Override + @SuppressWarnings("unchecked") + public Enum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return translator.fromText((Class>) ctxt.getContextualType().getRawClass(), p.getText()); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java index fede16987..606a97070 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/JsonSchema.java @@ -445,24 +445,50 @@ public class JsonSchema { * @author Oliver Gierke * @since 2.3 */ - static class EnumProperty extends JsonSchemaProperty { + public static class EnumProperty extends JsonSchemaProperty { - private final List values; + private List values; public EnumProperty(String name, String title, Class type, String description, boolean required) { + this(name, title, toValues(type), description, required); + } + + public EnumProperty(String name, String title, List values, String description, boolean required) { + super(name, title, description, required); - this.values = new ArrayList(); - - for (Object value : type.getEnumConstants()) { - this.values.add(value.toString()); - } + this.values = Collections.unmodifiableList(values); } @JsonProperty("enum") public List getValues() { return values; } + + /** + * Returns the current {@link EnumProperty} exposing the given values. + * + * @param values must not be {@literal null}. + * @return + */ + public EnumProperty withValues(List values) { + + Assert.notNull(values, "Values must not be null!"); + + this.values = Collections.unmodifiableList(values); + return this; + } + + private static List toValues(Class type) { + + List values = new ArrayList(); + + for (Object value : type.getEnumConstants()) { + values.add(value.toString()); + } + + return values; + } } } diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java index 2fd00931b..b3ab57c5e 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverter.java @@ -191,7 +191,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric TypeInformation actualPropertyType = propertyType.getActualType(); Class rawPropertyType = propertyType.getType(); - JsonSchemaFormat format = configuration.metadataConfiguration().getSchemaFormatFor(rawPropertyType); + JsonSchemaFormat format = configuration.getMetadataConfiguration().getSchemaFormatFor(rawPropertyType); ResourceDescription description = persistentProperty == null ? jackson.getFallbackDescription(metadata, definition) : getDescriptionFor(persistentProperty, metadata); JsonSchemaProperty property = getSchemaProperty(definition, propertyType, description); @@ -211,7 +211,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric continue; } - Pattern pattern = configuration.metadataConfiguration().getPatternFor(rawPropertyType); + Pattern pattern = configuration.getMetadataConfiguration().getPatternFor(rawPropertyType); if (pattern != null) { registrar.register(property.withPattern(pattern), actualPropertyType); @@ -261,6 +261,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric return getPropertiesFor(property.getActualType(), mappings.getMetadataFor(property.getActualType()), descriptors); } + @SuppressWarnings("unchecked") private JsonSchemaProperty getSchemaProperty(BeanPropertyDefinition definition, TypeInformation type, ResourceDescription description) { @@ -301,7 +302,7 @@ public class PersistentEntityToJsonSchemaConverter implements ConditionalGeneric return accessor.getMessage(resolvable); } catch (NoSuchMessageException o_O) { - if (configuration.metadataConfiguration().omitUnresolvableDescriptionKeys()) { + if (configuration.getMetadataConfiguration().omitUnresolvableDescriptionKeys()) { return null; } else { throw o_O; diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java index 43572e9d2..2fe6c2d74 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinks.java @@ -75,9 +75,8 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { * @param idConverters must not be {@literal null}. */ @Autowired - public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, - RepositoryRestConfiguration config, PagingAndSortingTemplateVariables templateVariables, - PluginRegistry> idConverters) { + public RepositoryEntityLinks(Repositories repositories, ResourceMappings mappings, RepositoryRestConfiguration config, + PagingAndSortingTemplateVariables templateVariables, PluginRegistry> idConverters) { Assert.notNull(repositories, "Repositories must not be null!"); Assert.notNull(mappings, "ResourceMappings must not be null!"); @@ -346,7 +345,7 @@ public class RepositoryEntityLinks extends AbstractEntityLinks { */ private TemplateVariables getProjectionVariable(Class type) { - ProjectionDefinitionConfiguration projectionConfiguration = config.projectionConfiguration(); + ProjectionDefinitionConfiguration projectionConfiguration = config.getProjectionConfiguration(); if (projectionConfiguration.hasProjectionFor(type)) { return new TemplateVariables(new TemplateVariable(projectionConfiguration.getParameterName(), REQUEST_PARAM)); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTests.java index 7527c9c10..81a676e04 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/AugmentingHandlerMappingUnitTests.java @@ -17,13 +17,18 @@ package org.springframework.data.rest.webmvc; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Map; +import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -50,7 +55,8 @@ public class AugmentingHandlerMappingUnitTests { @Test public void augmentsRequestMappingsWithBaseUriFromConfiguration() { - RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(); + RepositoryRestConfiguration configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); configuration.setBasePath("api"); BasePathAwareHandlerMapping mapping = new BasePathAwareHandlerMapping(configuration); @@ -60,7 +66,7 @@ public class AugmentingHandlerMappingUnitTests { Map handlerMethods = mapping.getHandlerMethods(); for (RequestMappingInfo info : handlerMethods.keySet()) { - assertThat(info.getPatternsCondition().getPatterns(), hasItem(startsWith("/api"))); + assertThat(info.getPatternsCondition().getPatterns(), hasItem(Matchers.startsWith("/api"))); } } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java index 4b747386d..a1c99c45a 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/RepositoryRestHandlerMappingUnitTests.java @@ -27,6 +27,9 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.domain.Sort; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.ResourceMappings; import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; @@ -63,7 +66,8 @@ public class RepositoryRestHandlerMappingUnitTests { @Before public void setUp() throws Exception { - configuration = new RepositoryRestConfiguration(); + configuration = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); handlerMapping = new RepositoryRestHandlerMapping(mappings, configuration); handlerMapping.setApplicationContext(CONTEXT); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/EnumTranslatorUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/EnumTranslatorUnitTests.java new file mode 100644 index 000000000..77e8aeefc --- /dev/null +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/EnumTranslatorUnitTests.java @@ -0,0 +1,185 @@ +/* + * Copyright 2015 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.rest.webmvc.json; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.context.support.StaticMessageSource; + +/** + * Unit tests for {@link EnumTranslator}. + * + * @author Oliver Gierke + */ +public class EnumTranslatorUnitTests { + + StaticMessageSource messageSource; + EnumTranslator configuration; + + @Before + public void setUp() { + + LocaleContextHolder.setLocale(Locale.US); + + this.messageSource = new StaticMessageSource(); + this.messageSource.addMessage(MyEnum.class.getName().concat(".").concat(MyEnum.FIRST_VALUE.name()), Locale.US, + "Translated"); + this.configuration = new EnumTranslator(new MessageSourceAccessor(messageSource)); + } + + /** + * @see DATAREST-654 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMessageSourceAccessor() { + new EnumTranslator(null); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesNullForNullSource() { + assertThat(configuration.fromText(MyEnum.class, null), is(nullValue())); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesNullForEmptySource() { + assertThat(configuration.fromText(MyEnum.class, null), is(nullValue())); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesNullForUnknownValue() { + assertThat(configuration.fromText(MyEnum.class, "Foobar"), is(nullValue())); + } + + /** + * @see DATAREST-654 + */ + @Test + public void returnsEnumNameIfDefaultTranslationIsDisabled() { + + configuration.setEnableDefaultTranslation(false); + + assertThat(configuration.asText(MyEnum.SECOND_VALUE), is(MyEnum.SECOND_VALUE.name())); + } + + /** + * @see DATAREST-654 + */ + @Test + public void returnsDefaultTranslationByDefault() { + + assertThat(configuration.asText(MyEnum.SECOND_VALUE), is("Second value")); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesEnumNameIfDefaultTranslationIsDisabled() { + + configuration.setEnableDefaultTranslation(false); + + assertThat(configuration.fromText(MyEnum.class, "FIRST_VALUE"), is(MyEnum.FIRST_VALUE)); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesStandardTranslationAndEnumNameByDefault() { + + assertThat(configuration.fromText(MyEnum.class, "FIRST_VALUE"), is(MyEnum.FIRST_VALUE)); + assertThat(configuration.fromText(MyEnum.class, "Second value"), is(MyEnum.SECOND_VALUE)); + } + + /** + * @see DATAREST-654 + */ + @Test + public void translatesEnumName() { + + LocaleContextHolder.setLocale(Locale.US); + + messageSource.addMessage(MyEnum.class.getName().concat(".").concat(MyEnum.FIRST_VALUE.name()), Locale.US, + "Translated"); + + assertThat(configuration.asText(MyEnum.FIRST_VALUE), is("Translated")); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesEnumNameByDefaultEvenIfMessageDefined() { + + // Parses resolved message and enum name + assertThat(configuration.fromText(MyEnum.class, "Translated"), is(MyEnum.FIRST_VALUE)); + assertThat(configuration.fromText(MyEnum.class, "FIRST_VALUE"), is(MyEnum.FIRST_VALUE)); + + // Does not parse default translation as explicit translation is available + assertThat(configuration.fromText(MyEnum.class, "First value"), is(nullValue())); + + // Parses default translation as no explicit translation is available + assertThat(configuration.fromText(MyEnum.class, "Second value"), is(MyEnum.SECOND_VALUE)); + assertThat(configuration.fromText(MyEnum.class, "SECOND_VALUE"), is(MyEnum.SECOND_VALUE)); + } + + /** + * @see DATAREST-654 + */ + @Test + public void parsesEnumWithDefaultTranslationDisabled() { + + configuration.setEnableDefaultTranslation(false); + + // Parses default translation as no explicit translation is available + assertThat(configuration.fromText(MyEnum.class, "Second value"), is(nullValue())); + assertThat(configuration.fromText(MyEnum.class, "SECOND_VALUE"), is(MyEnum.SECOND_VALUE)); + } + + @Test + public void doesNotResolveEnumNameAsFallbackIfConfigured() { + + configuration.setParseEnumNameAsFallback(false); + + // Parses resolved message and enum name + assertThat(configuration.fromText(MyEnum.class, "Translated"), is(MyEnum.FIRST_VALUE)); + assertThat(configuration.fromText(MyEnum.class, "FIRST_VALUE"), is(nullValue())); + + // Parses default translation as no explicit translation is available + assertThat(configuration.fromText(MyEnum.class, "Second value"), is(MyEnum.SECOND_VALUE)); + assertThat(configuration.fromText(MyEnum.class, "SECOND_VALUE"), is(nullValue())); + } + + static enum MyEnum { + FIRST_VALUE, SECOND_VALUE; + } +} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/JacksonMetadataUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/JacksonMetadataUnitTests.java index e2ab164b7..da8848de4 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/JacksonMetadataUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/JacksonMetadataUnitTests.java @@ -20,15 +20,18 @@ import static org.junit.Assert.*; import java.io.IOException; +import org.junit.Before; import org.junit.Test; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonProperty.Access; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -38,29 +41,51 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; * Unit tests for {@link JacksonMetadata}. * * @author Oliver Gierke - * @soundtrack Four Sided Cube - Bad Day's Rememberance (Bunch of Sides) + * @soundtrack Four Sided Cube - Bad Day's Remembrance (Bunch of Sides) */ public class JacksonMetadataUnitTests { + MappingContext context; + ObjectMapper mapper; + + @Before + public void setUp() { + + this.context = new MongoMappingContext(); + + this.mapper = new ObjectMapper(); + this.mapper.disable(MapperFeature.INFER_PROPERTY_MUTATORS); + } + /** * @see DATAREST-644 */ @Test - public void testname() { - - MongoMappingContext context = new MongoMappingContext(); - MongoPersistentEntity entity = context.getPersistentEntity(User.class); - - ObjectMapper mapper = new ObjectMapper(); + public void detectsReadOnlyProperty() { JacksonMetadata metadata = new JacksonMetadata(mapper, User.class); - MongoPersistentProperty property = entity.getPersistentProperty("username"); + PersistentEntity entity = context.getPersistentEntity(User.class); + PersistentProperty property = entity.getPersistentProperty("username"); assertThat(metadata.isExported(property), is(true)); assertThat(metadata.isReadOnly(property), is(true)); } + /** + * @see DATAREST-644 + */ + @Test + public void reportsConstructorArgumentAsJacksonWritable() { + + JacksonMetadata metadata = new JacksonMetadata(mapper, Value.class); + + PersistentEntity entity = context.getPersistentEntity(Value.class); + PersistentProperty property = entity.getPersistentProperty("value"); + + assertThat(metadata.isReadOnly(property), is(false)); + } + /** * @see DATAREST-644 */ @@ -77,12 +102,25 @@ public class JacksonMetadataUnitTests { private String username; - @JsonProperty(access = Access.READ_ONLY) public String getUsername() { return username; } } + static class Value { + + private String value; + + @JsonCreator + public Value(@JsonProperty("value") String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + @JsonSerialize(using = SomeBeanSerializer.class) static class SomeBean {} diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java index abdcbd4a4..2f5d8239e 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2ModuleUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.rest.webmvc.json; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.mockito.Mockito.*; import java.util.Arrays; @@ -27,6 +28,9 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.webmvc.mapping.AssociationLinks; @@ -58,7 +62,8 @@ public class PersistentEntityJackson2ModuleUnitTests { SimpleModule module = new SimpleModule(); module.setSerializerModifier(new PersistentEntityJackson2Module.AssociationOmittingSerializerModifier( - persistentEntities, associationLinks, new RepositoryRestConfiguration())); + persistentEntities, associationLinks, new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)))); this.mapper = new ObjectMapper(); this.mapper.registerModule(module); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java index 936e2ece4..77e27f51f 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/PersistentEntityToJsonSchemaConverterUnitTests.java @@ -71,8 +71,8 @@ public class PersistentEntityToJsonSchemaConverterUnitTests { @Override public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) { - config.metadataConfiguration().registerJsonSchemaFormat(JsonSchemaFormat.EMAIL, EmailAddress.class); - config.metadataConfiguration().registerFormattingPatternFor("[A-Z]+", TypeWithPattern.class); + config.getMetadataConfiguration().registerJsonSchemaFormat(JsonSchemaFormat.EMAIL, EmailAddress.class); + config.getMetadataConfiguration().registerFormattingPatternFor("[A-Z]+", TypeWithPattern.class); config.exposeIdsFor(Profile.class); } diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java index 4b9a5fd97..09e048be1 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/RepositoryTestsConfig.java @@ -31,6 +31,9 @@ import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.repository.support.DomainClassConverter; import org.springframework.data.repository.support.Repositories; import org.springframework.data.rest.core.UriToEntityConverter; +import org.springframework.data.rest.core.config.EnumTranslationConfiguration; +import org.springframework.data.rest.core.config.MetadataConfiguration; +import org.springframework.data.rest.core.config.ProjectionDefinitionConfiguration; import org.springframework.data.rest.core.config.RepositoryRestConfiguration; import org.springframework.data.rest.core.mapping.RepositoryResourceMappings; import org.springframework.data.rest.webmvc.jpa.JpaRepositoryConfig; @@ -74,7 +77,8 @@ public class RepositoryTestsConfig { @Bean public RepositoryRestConfiguration config() { - RepositoryRestConfiguration config = new RepositoryRestConfiguration(); + RepositoryRestConfiguration config = new RepositoryRestConfiguration(new ProjectionDefinitionConfiguration(), + new MetadataConfiguration(), mock(EnumTranslationConfiguration.class)); config.setResourceMappingForDomainType(Person.class).setRel("person"); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java index 2b3c9f4ae..7def69de3 100644 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/support/RepositoryEntityLinksIntegrationTests.java @@ -73,7 +73,7 @@ public class RepositoryEntityLinksIntegrationTests extends AbstractControllerInt Link link = entityLinks.linkToSingleResource(Order.class, 1); assertThat(link.isTemplated(), is(true)); - assertThat(link.getVariableNames(), hasItem(configuration.projectionConfiguration().getParameterName())); + assertThat(link.getVariableNames(), hasItem(configuration.getProjectionConfiguration().getParameterName())); } /**