diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java index a7980fff37..192b1d99d5 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2019 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. @@ -34,6 +34,7 @@ import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; import javax.validation.Validation; @@ -50,11 +51,13 @@ import org.springframework.beans.BeanWrapperImpl; import org.springframework.context.support.StaticMessageSource; import org.springframework.util.ObjectUtils; import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; import org.springframework.validation.beanvalidation.SpringValidatorAdapter; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.StringContains.*; import static org.junit.Assert.*; /** @@ -73,7 +76,7 @@ public class SpringValidatorAdapterTests { @Before public void setupSpringValidatorAdapter() { messageSource.addMessage("Size", Locale.ENGLISH, "Size of {0} is must be between {2} and {1}"); - messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value with {1}"); + messageSource.addMessage("Same", Locale.ENGLISH, "{2} must be same value as {1}"); messageSource.addMessage("password", Locale.ENGLISH, "Password"); messageSource.addMessage("confirmPassword", Locale.ENGLISH, "Password(Confirm)"); } @@ -96,8 +99,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("pass")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Size of Password is must be between 8 and 128")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password is must be between 8 and 128")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -111,8 +117,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("password")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Password must be same value with Password(Confirm)")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value as Password(Confirm)")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -127,10 +136,16 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value as confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.contains(ConstraintViolation.class)); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + assertTrue(error2.contains(ConstraintViolation.class)); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); } @Test // SPR-15123 @@ -147,10 +162,34 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value as confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.contains(ConstraintViolation.class)); + assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + assertTrue(error2.contains(ConstraintViolation.class)); + assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); + } + + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email"), is(1)); + assertThat(errors.getFieldValue("email"), is("X")); + FieldError error = errors.getFieldError("email"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), containsString("[\\w.'-]{1,}@[\\w.'-]{1,}")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); } @Test // SPR-16177 @@ -243,6 +282,7 @@ public class SpringValidatorAdapterTests { private String confirmPassword; + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") private String email; @Pattern(regexp = "[\\p{L} -]*", message = "Email required") @@ -403,13 +443,13 @@ public class SpringValidatorAdapterTests { private Integer id; - @javax.validation.constraints.NotNull + @NotNull private String name; - @javax.validation.constraints.NotNull + @NotNull private Integer age; - @javax.validation.constraints.NotNull + @NotNull private Parent parent; public Integer getId() { diff --git a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java index 4107516ea5..08a2e3427b 100644 --- a/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java +++ b/spring-context-support/src/test/java/org/springframework/validation/beanvalidation2/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -284,6 +284,9 @@ public class ValidatorFactoryTests { errors.initConversion(new DefaultConversionService()); validator.validate(listContainer, errors); + FieldError fieldError = errors.getFieldError("list[1]"); + assertNotNull(fieldError); + assertEquals("X", fieldError.getRejectedValue()); assertEquals("X", errors.getFieldValue("list[1]")); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java index 8ae8c21f5c..33c5e3907b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AutoProxyRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -57,9 +57,9 @@ public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { boolean candidateFound = false; - Set annoTypes = importingClassMetadata.getAnnotationTypes(); - for (String annoType : annoTypes) { - AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType); + Set annTypes = importingClassMetadata.getAnnotationTypes(); + for (String annType : annTypes) { + AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType); if (candidate == null) { continue; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index a6e32aa61b..3258769e0f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -534,7 +534,7 @@ class ConfigurationClassParser { if (visited.add(sourceClass)) { for (SourceClass annotation : sourceClass.getAnnotations()) { String annName = annotation.getMetadata().getClassName(); - if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) { + if (!annName.equals(Import.class.getName())) { collectImports(annotation, imports, visited); } } @@ -542,8 +542,6 @@ class ConfigurationClassParser { } } - - private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection importCandidates, boolean checkForCircularImports) { @@ -565,8 +563,7 @@ class ConfigurationClassParser { ParserStrategyUtils.invokeAwareMethods( selector, this.environment, this.resourceLoader, this.registry); if (selector instanceof DeferredImportSelector) { - this.deferredImportSelectorHandler.handle( - configClass, (DeferredImportSelector) selector); + this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } else { String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata()); @@ -1018,13 +1015,32 @@ class ConfigurationClassParser { public Set getAnnotations() { Set result = new LinkedHashSet<>(); - for (String className : this.metadata.getAnnotationTypes()) { - try { - result.add(getRelated(className)); + if (this.source instanceof Class) { + Class sourceClass = (Class) this.source; + for (Annotation ann : sourceClass.getAnnotations()) { + Class annType = ann.annotationType(); + if (!annType.getName().startsWith("java")) { + try { + result.add(asSourceClass(annType)); + } + catch (Throwable ex) { + // An annotation not present on the classpath is being ignored + // by the JVM's class loading -> ignore here as well. + } + } } - catch (Throwable ex) { - // An annotation not present on the classpath is being ignored - // by the JVM's class loading -> ignore here as well. + } + else { + for (String className : this.metadata.getAnnotationTypes()) { + if (!className.startsWith("java")) { + try { + result.add(getRelated(className)); + } + catch (Throwable ex) { + // An annotation not present on the classpath is being ignored + // by the JVM's class loading -> ignore here as well. + } + } } } return result; diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java index 43544deb04..df4dbbf96b 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -287,6 +287,12 @@ public abstract class AbstractMessageSource extends MessageSourceSupport impleme String defaultMessage = resolvable.getDefaultMessage(); String[] codes = resolvable.getCodes(); if (defaultMessage != null) { + if (resolvable instanceof DefaultMessageSourceResolvable && + !((DefaultMessageSourceResolvable) resolvable).shouldRenderDefaultMessage()) { + // Given default message does not contain any argument placeholders + // (and isn't escaped for alwaysUseMessageFormat either) -> return as-is. + return defaultMessage; + } if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) { // Never format a code-as-default-message, even with alwaysUseMessageFormat=true return defaultMessage; diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java index 1a554a0ab7..cc443eafdd 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -129,13 +129,28 @@ public class DefaultMessageSourceResolvable implements MessageSourceResolvable, return this.defaultMessage; } + /** + * Indicate whether the specified default message needs to be rendered for + * substituting placeholders and/or {@link java.text.MessageFormat} escaping. + * @return {@code true} if the default message may contain argument placeholders; + * {@code false} if it definitely does not contain placeholders or custom escaping + * and can therefore be simply exposed as-is + * @since 5.1.7 + * @see #getDefaultMessage() + * @see #getArguments() + * @see AbstractMessageSource#renderDefaultMessage + */ + public boolean shouldRenderDefaultMessage() { + return true; + } + /** * Build a default String representation for this MessageSourceResolvable: * including codes, arguments, and default message. */ protected final String resolvableToString() { - StringBuilder result = new StringBuilder(); + StringBuilder result = new StringBuilder(64); result.append("codes [").append(StringUtils.arrayToDelimitedString(this.codes, ",")); result.append("]; arguments [").append(StringUtils.arrayToDelimitedString(this.arguments, ",")); result.append("]; default message [").append(this.defaultMessage).append(']'); diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index e10f51f600..ce974b975f 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -148,6 +148,7 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. * @param violations the JSR-303 ConstraintViolation results * @param errors the Spring errors object to register to */ + @SuppressWarnings("serial") protected void processConstraintViolations(Set> violations, Errors errors) { for (ConstraintViolation violation : violations) { String field = determineField(violation); @@ -165,7 +166,12 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. if (nestedField.isEmpty()) { String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); ObjectError error = new ObjectError( - errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()); + errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()) { + @Override + public boolean shouldRenderDefaultMessage() { + return false; + } + }; error.wrap(violation); bindingResult.addError(error); } @@ -173,7 +179,12 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. Object rejectedValue = getRejectedValue(field, violation, bindingResult); String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); FieldError error = new FieldError(errors.getObjectName(), nestedField, - rejectedValue, false, errorCodes, errorArgs, violation.getMessage()); + rejectedValue, false, errorCodes, errorArgs, violation.getMessage()) { + @Override + public boolean shouldRenderDefaultMessage() { + return false; + } + }; error.wrap(violation); bindingResult.addError(error); } diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java index be955f24cc..725f4a96d9 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -54,6 +54,7 @@ import org.springframework.validation.FieldError; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.StringContains.*; import static org.junit.Assert.*; /** @@ -170,6 +171,24 @@ public class SpringValidatorAdapterTests { assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail")); } + @Test + public void testPatternMessage() { + TestBean testBean = new TestBean(); + testBean.setEmail("X"); + testBean.setConfirmEmail("X"); + + BeanPropertyBindingResult errors = new BeanPropertyBindingResult(testBean, "testBean"); + validatorAdapter.validate(testBean, errors); + + assertThat(errors.getFieldErrorCount("email"), is(1)); + assertThat(errors.getFieldValue("email"), is("X")); + FieldError error = errors.getFieldError("email"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), containsString("[\\w.'-]{1,}@[\\w.'-]{1,}")); + assertTrue(error.contains(ConstraintViolation.class)); + assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email")); + } + @Test // SPR-16177 public void testWithList() { Parent parent = new Parent(); @@ -218,6 +237,7 @@ public class SpringValidatorAdapterTests { private String confirmPassword; + @Pattern(regexp = "[\\w.'-]{1,}@[\\w.'-]{1,}") private String email; @Pattern(regexp = "[\\p{L} -]*", message = "Email required") diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java index e53692b648..6a8655dc95 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/ValidatorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -284,6 +284,8 @@ public class ValidatorFactoryTests { validator.validate(listContainer, errors); FieldError fieldError = errors.getFieldError("list[1]"); + assertNotNull(fieldError); + assertEquals("X", fieldError.getRejectedValue()); assertEquals("X", errors.getFieldValue("list[1]")); } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 28b3ebc211..677e3768ce 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2019 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. @@ -205,19 +205,13 @@ public class AnnotationMetadataTests { assertThat("length of basePackageClasses[]", basePackageClasses.length, is(0)); } - /** - * https://jira.spring.io/browse/SPR-11649 - */ - @Test + @Test // SPR-11649 public void multipleAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { AnnotationMetadata metadata = new StandardAnnotationMetadata(NamedAnnotationsClass.class); assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); } - /** - * https://jira.spring.io/browse/SPR-11649 - */ - @Test + @Test // SPR-11649 public void multipleAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedAnnotationsClass.class.getName()); @@ -225,19 +219,13 @@ public class AnnotationMetadataTests { assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); } - /** - * https://jira.spring.io/browse/SPR-11649 - */ - @Test + @Test // SPR-11649 public void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { AnnotationMetadata metadata = new StandardAnnotationMetadata(NamedComposedAnnotationClass.class); assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); } - /** - * https://jira.spring.io/browse/SPR-11649 - */ - @Test + @Test // SPR-11649 public void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingAnnotationMetadataReadingVisitor() throws Exception { MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(NamedComposedAnnotationClass.class.getName()); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index dbbfefbe82..433fef384d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -18,6 +18,7 @@ package org.springframework.http.converter.json; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -62,6 +63,8 @@ import org.springframework.http.HttpLogging; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.util.xml.StaxUtils; @@ -632,24 +635,27 @@ public class Jackson2ObjectMapperBuilder { public void configure(ObjectMapper objectMapper) { Assert.notNull(objectMapper, "ObjectMapper must not be null"); - Map modulesToRegister = new LinkedHashMap<>(); + MultiValueMap modulesToRegister = new LinkedMultiValueMap<>(); if (this.findModulesViaServiceLoader) { - ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> modulesToRegister.put(module.getTypeId(), module)); + ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> registerModule(module, modulesToRegister)); } else if (this.findWellKnownModules) { registerWellKnownModulesIfAvailable(modulesToRegister); } if (this.modules != null) { - this.modules.forEach(module -> modulesToRegister.put(module.getTypeId(), module)); + this.modules.forEach(module -> registerModule(module, modulesToRegister)); } if (this.moduleClasses != null) { for (Class moduleClass : this.moduleClasses) { - Module module = BeanUtils.instantiateClass(moduleClass); - modulesToRegister.put(module.getTypeId(), module); + registerModule(BeanUtils.instantiateClass(moduleClass), modulesToRegister); } } - objectMapper.registerModules(modulesToRegister.values()); + List modules = new ArrayList<>(); + for (List nestedModules : modulesToRegister.values()) { + modules.addAll(nestedModules); + } + objectMapper.registerModules(modules); if (this.dateFormat != null) { objectMapper.setDateFormat(this.dateFormat); @@ -701,6 +707,15 @@ public class Jackson2ObjectMapperBuilder { } } + private void registerModule(Module module, MultiValueMap modulesToRegister) { + if (module.getTypeId() == null) { + modulesToRegister.add(SimpleModule.class.getName(), module); + } + else { + modulesToRegister.set(module.getTypeId(), module); + } + } + // Any change to this method should be also applied to spring-jms and spring-messaging // MappingJackson2MessageConverter default constructors @@ -747,12 +762,12 @@ public class Jackson2ObjectMapperBuilder { } @SuppressWarnings("unchecked") - private void registerWellKnownModulesIfAvailable(Map modulesToRegister) { + private void registerWellKnownModulesIfAvailable(MultiValueMap modulesToRegister) { try { Class jdk8ModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jdk8.Jdk8Module", this.moduleClassLoader); Module jdk8Module = BeanUtils.instantiateClass(jdk8ModuleClass); - modulesToRegister.put(jdk8Module.getTypeId(), jdk8Module); + modulesToRegister.set(jdk8Module.getTypeId(), jdk8Module); } catch (ClassNotFoundException ex) { // jackson-datatype-jdk8 not available @@ -762,7 +777,7 @@ public class Jackson2ObjectMapperBuilder { Class javaTimeModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule", this.moduleClassLoader); Module javaTimeModule = BeanUtils.instantiateClass(javaTimeModuleClass); - modulesToRegister.put(javaTimeModule.getTypeId(), javaTimeModule); + modulesToRegister.set(javaTimeModule.getTypeId(), javaTimeModule); } catch (ClassNotFoundException ex) { // jackson-datatype-jsr310 not available @@ -774,7 +789,7 @@ public class Jackson2ObjectMapperBuilder { Class jodaModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.datatype.joda.JodaModule", this.moduleClassLoader); Module jodaModule = BeanUtils.instantiateClass(jodaModuleClass); - modulesToRegister.put(jodaModule.getTypeId(), jodaModule); + modulesToRegister.set(jodaModule.getTypeId(), jodaModule); } catch (ClassNotFoundException ex) { // jackson-datatype-joda not available @@ -787,7 +802,7 @@ public class Jackson2ObjectMapperBuilder { Class kotlinModuleClass = (Class) ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader); Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); - modulesToRegister.put(kotlinModule.getTypeId(), kotlinModule); + modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule); } catch (ClassNotFoundException ex) { if (!kotlinWarningLogged) { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index eaab345436..7e4b582146 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.TimeZone; +import java.util.stream.StreamSupport; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonFilter; @@ -329,6 +330,24 @@ public class Jackson2ObjectMapperBuilderTests { assertNotNull(demoPojo.getOffsetDateTime()); } + @Test // gh-22740 + public void registerMultipleModulesWithNullTypeId() { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + SimpleModule fooModule = new SimpleModule(); + fooModule.addSerializer(new FooSerializer()); + SimpleModule barModule = new SimpleModule(); + barModule.addSerializer(new BarSerializer()); + builder.modulesToInstall(fooModule, barModule); + ObjectMapper objectMapper = builder.build(); + assertEquals(1, StreamSupport + .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) + .filter(s -> s.findSerializer(null, SimpleType.construct(Foo.class), null) != null) + .count()); + assertEquals(1, StreamSupport + .stream(getSerializerFactoryConfig(objectMapper).serializers().spliterator(), false) + .filter(s -> s.findSerializer(null, SimpleType.construct(Bar.class), null) != null) + .count()); + } private static SerializerFactoryConfig getSerializerFactoryConfig(ObjectMapper objectMapper) { return ((BasicSerializerFactory) objectMapper.getSerializerFactory()).getFactoryConfig(); @@ -680,4 +699,29 @@ public class Jackson2ObjectMapperBuilderTests { public static class MyXmlFactory extends XmlFactory { } + static class Foo {} + + static class Bar {} + + static class FooSerializer extends JsonSerializer { + @Override + public void serialize(Foo value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + } + + @Override + public Class handledType() { + return Foo.class; + } + } + + static class BarSerializer extends JsonSerializer { + @Override + public void serialize(Bar value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + } + @Override + public Class handledType() { + return Bar.class; + } + } + }