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())); } /**