Spring REST Handle locale change - spring

I'm trying to handle locale change in a Spring 3 REST application.
But the locale is not changed to fr.
The console log shows:
2014-05-19 14:29:46,214 DEBUG [AbstractExceptionHandler] locale: en
Here is my configuration:
#Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages/messages", "classpath:messages/validation");
// If true, the key of the message will be displayed if the key is not found, instead of throwing an exception
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
// The value 0 means always reload the messages to be developer friendly
messageSource.setCacheSeconds(0);
return messageSource;
}
// The locale interceptor provides a way to switch the language in any page just by passing the lang=’en’, lang=’fr’, and so on to the url
#Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
#Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver localeResolver = new CookieLocaleResolver();
localeResolver.setDefaultLocale(new Locale("en"));
return localeResolver;
}
Here is my exception handler:
#ControllerAdvice
public class AdminExceptionHandler extends AbstractExceptionHandler {
#ExceptionHandler(NullPointerException.class)
#ResponseBody
public ResponseEntity<ErrorInfo> nullPointerException(HttpServletRequest request, NullPointerException e) {
String url = request.getRequestURL().toString();
String errorMessage = localizeErrorMessage("error.npe", new Object[] { e.getMessage() });
return new ResponseEntity<ErrorInfo>(new ErrorInfo(url, errorMessage), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public class AbstractExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(AbstractExceptionHandler.class);
#Autowired
private MessageSource messageSource;
protected String localizeErrorMessage(String errorCode, Object args[]) {
Locale locale = LocaleContextHolder.getLocale();
logger.debug("locale: " + locale);
return messageSource.getMessage(errorCode, args, locale);
}
protected String localizeErrorMessage(String errorCode) {
return localizeErrorMessage(errorCode, null);
}
protected String extractAdminIdFromUrl(String url) {
String adminId = null;
try {
URI uri = new URI(url);
String path = uri.getPath();
adminId = path.substring(path.lastIndexOf('/') + 1);
} catch (URISyntaxException e1) {
e1.printStackTrace();
}
return adminId;
}
}
And here is my test:
#Test
public void testExceptionLocalizedMessage() throws Exception {
HttpHeaders httpHeaders = Common.createAuthenticationHeaders("stephane" + ":" + PASSWORD);
MvcResult resultGet = this.mockMvc.perform(
get("/error/npe").headers(httpHeaders)
.param("lang", "fr")
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("Une erreur inconnue s'est produite. Veuillez nous excuser."))
.andReturn();
httpHeaders.add("Accept-Language", "fr");
resultGet = this.mockMvc.perform(
get("/error/npe").headers(httpHeaders)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("Une erreur inconnue s'est produite. Veuillez nous excuser."))
.andReturn();
}
I would like to handle the locale argument in the url as in ?lang=en and the Accept-Language header as a fall back.
As a REST application I was thinking of using the AcceptHeaderLocaleResolver class but it does not support the setting of the locale via the url parameter.
I reckoned using the SessionLocaleResolver class makes little sense in a REST application.
That leaves my with the CookieLocaleResolver class which I'm not specially convinced is the one that should be used in a REST application.
Anyway, the retrieved locale is still en and not fr as I expect it to be.
EDIT:
In the test, using the statement:
httpHeaders.add("Accept-Language", Locale.FRENCH.getLanguage());
does not set the locale.
But using the locale() does.
This test passes:
this.mockMvc.perform(
get("/error/npe").headers(httpHeaders).locale(Locale.FRENCH)
.accept(MediaType.APPLICATION_JSON))
.andDo(print()
)
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value(localizeErrorMessage("error.npe", Locale.FRENCH)))
.andReturn();

I found the solution. Now the Accept-Language header is being used and the cookie as well.
public class WebConfiguration extends WebMvcConfigurerAdapter {
#Bean
public LocaleResolver localeResolver() {
return new SmartLocaleResolver();
}
}
public class SmartLocaleResolver extends CookieLocaleResolver {
#Override
public Locale resolveLocale(HttpServletRequest request) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage == null || acceptLanguage.trim().isEmpty()) {
return super.determineDefaultLocale(request);
}
return request.getLocale();
}
}
UPDATE: Following Thor's comment, here is a resolver that checks first for the cookie, and if not found checks for the request header:
#Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = super.determineDefaultLocale(request);
if (null == locale) {
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage != null && !acceptLanguage.trim().isEmpty()) {
locale = request.getLocale();
}
}
return locale;
}
Or with a simpler implementation (not tested):
private AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
#Override
public Locale resolveLocale(HttpServletRequest request) {
Locale locale = super.determineDefaultLocale(request);
if (null == locale) {
locale = acceptHeaderLocaleResolver.resolveLocale(request);
}
return locale;
}
UPDATE: This above solution is not working any longer.
I'm now trying to pass the accepted language in a header:
httpHeaders.add(HttpHeaders.ACCEPT_LANGUAGE, "fr_FR");
and retrieve it in this locale resolver:
#Override
public Locale resolveLocale(HttpServletRequest request) {
for (String httpHeaderName : Collections.list(request.getHeaderNames())) {
logger.debug("===========>> Header name: " + httpHeaderName);
}
String acceptLanguage = request.getHeader(HttpHeaders.ACCEPT_LANGUAGE);
logger.debug("===========>> acceptLanguage: " + acceptLanguage);
Locale locale = super.resolveLocale(request);
logger.debug("===========>> acceptLanguage locale: " + locale.getDisplayCountry());
if (null == locale) {
locale = getDefaultLocale();
logger.debug("===========>> Default locale: " + locale.getDisplayCountry());
}
return locale;
}
But there is no Accept-Language in the output of the ===========>> Header name logger, and the acceptLanguage logger is empty.

when we are using
#Bean
public SessionLocaleResolver localeResolver(){
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
localeResolver.setDefaultLocale(Locale.US);
return localeResolver;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("language");
return localeChangeInterceptor;
}
it is able to accept locale from query parameter
{{url}}/com-manh-cp-ext-order/api/ext/ex23order/greeting?language=es

Related

Spring WS - return non-MTOM on MTOM request

I have a SOAP service that receives a binary attachment (MTOM). The issue is that I would like to return a non-MTOM response (a "clean" SOAP return). The current code return a multipart response and uses "xop+xml" content-type.
The code is:
#Configuration
public class MyWebServiceConfig {
#Value("${config1}")
private String contextPath;
#Value("${config2}")
private String namespace;
#Value("${config3}")
private String wsdl;
#Bean
ServletRegistrationBean<?> webServicesRegistration(ApplicationContext ctx) {
MessageDispatcherServlet messageDispatcherServlet = new MessageDispatcherServlet();
messageDispatcherServlet.setApplicationContext(ctx);
messageDispatcherServlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(messageDispatcherServlet, contextPath, "*.wsdl");
}
#Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath(namespace);
marshaller.setMtomEnabled(true);
return marshaller;
}
#Bean
public MarshallingPayloadMethodProcessor methodProcessor(Jaxb2Marshaller marshaller) {
return new MarshallingPayloadMethodProcessor(marshaller);
}
#Bean
DefaultMethodEndpointAdapter endpointAdapter(MarshallingPayloadMethodProcessor methodProcessor) {
DefaultMethodEndpointAdapter adapter = new DefaultMethodEndpointAdapter();
adapter.setMethodArgumentResolvers(Collections.singletonList(methodProcessor));
adapter.setMethodReturnValueHandlers(Collections.singletonList(methodProcessor));
return adapter;
}
#Bean
public SimpleWsdl11Definition contentStore() {
SimpleWsdl11Definition definition = new SimpleWsdl11Definition();
definition.setWsdl(new ClassPathResource(wsdl));
return definition;
}
}
I have tried changing to setMtomEnabled(false) - but then the request is not handled correctly.
Is the anyway of receiving MTOM - but returning a non-MTOM response?
Edit:
Endpoint looks like this:
#Endpoint
public class MyEndpoint {
private ObjectFactory objectFactory;
public MyEndpoint() {
this.objectFactory = new ObjectFactory();
}
#PayloadRoot(localPart = "SendMyRequest", namespace = "somenamespace")
#ResponsePayload
public JAXBElement<MyReceipt> store(#RequestPayload JAXBElement<MyRequest> storeContentRequest) throws IOException {
MyReceipt receipt = new MyReceipt();
receipt.setf1(123);
receipt.setf2(456);
return this.objectFactory.createMyResponse(receipt);
}
}

JwtAuthenticationToken is not in the allowlist, Jackson issue

I have created my authorization server using org.springframework.security:spring-security-oauth2-authorization-server:0.2.2 and my client using org.springframework.boot:spring-boot-starter-oauth2-client. The users are able to sign in and out successfully, however, while testing I noticed that if I log in successfully then restart the client (but not the server) without signing out and try to login in again the server throws the following error in an endless loop of redirects
java.lang.IllegalArgumentException: The class with org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken and name of org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details
I tried to follow this link https://github.com/spring-projects/spring-security/issues/4370 but the solution on it did not work for me. I also tried a different solution described in this link https://github.com/spring-projects/spring-authorization-server/issues/397#issuecomment-900148920 and modified my authorization server code as follows:-
Here is my Jackson Configs
#Configuration
public class JacksonConfiguration {
/**
* Support for Java date and time API.
*
* #return the corresponding Jackson module.
*/
#Bean
public JavaTimeModule javaTimeModule() {
return new JavaTimeModule();
}
#Bean
public Jdk8Module jdk8TimeModule() {
return new Jdk8Module();
}
/*
* Support for Hibernate types in Jackson.
*/
#Bean
public Hibernate5Module hibernate5Module() {
return new Hibernate5Module();
}
/*
* Module for serialization/deserialization of RFC7807 Problem.
*/
#Bean
public ProblemModule problemModule() {
return new ProblemModule();
}
/*
* Module for serialization/deserialization of ConstraintViolationProblem.
*/
#Bean
public ConstraintViolationProblemModule constraintViolationProblemModule() {
return new ConstraintViolationProblemModule();
}
/**
* To (de)serialize a BadCredentialsException, use CoreJackson2Module:
*/
#Bean
public CoreJackson2Module coreJackson2Module() {
return new CoreJackson2Module();
}
#Bean
#Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(coreJackson2Module());
mapper.registerModule(javaTimeModule());
mapper.registerModule(jdk8TimeModule());
mapper.registerModule(hibernate5Module());
mapper.registerModule(problemModule());
mapper.registerModule(constraintViolationProblemModule());
return mapper;
}
}
and here is my Authorization server config
#Configuration(proxyBeanMethods = false)
public class AuthServerConfig {
private final DataSource dataSource;
private final AuthProperties authProps;
private final PasswordEncoder encoder;
public AuthServerConfig(DataSource dataSource, AuthProperties authProps, PasswordEncoder encoder) {
this.dataSource = dataSource;
this.authProps = authProps;
this.encoder = encoder;
}
#Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource);
}
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
authorizationServerConfigurer.tokenRevocationEndpoint(tokenRevocationEndpoint -> tokenRevocationEndpoint
.revocationResponseHandler((request, response, authentication) -> {
Assert.notNull(request, "HttpServletRequest required");
HttpSession session = request.getSession(false);
if (!Objects.isNull(session)) {
session.removeAttribute("SPRING_SECURITY_CONTEXT");
session.invalidate();
}
SecurityContextHolder.getContext().setAuthentication(null);
SecurityContextHolder.clearContext();
response.setStatus(HttpStatus.OK.value());
})
);
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
#Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, TokenSettings tokenSettings) {
JdbcRegisteredClientRepository clientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
RegisteredClient webClient = RegisteredClient.withId("98a9104c-a9c7-4d7c-ad03-ec61bcfeab36")
.clientId(authProps.getClientId())
.clientName(authProps.getClientName())
.clientSecret(encoder.encode(authProps.getClientSecret()))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8000/login/oauth2/code/web-client")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.tokenSettings(tokenSettings)
.build();
clientRepository.save(webClient);
return clientRepository;
}
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository,
ObjectMapper objectMapper) {
JdbcOAuth2AuthorizationService authorizationService =
new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// You will need to write the Mixin for your class so Jackson can marshall it.
// objectMapper.addMixIn(UserPrincipal .class, UserPrincipalMixin.class);
rowMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
return authorizationService;
}
#Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
#Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static RSAKey generateRsa() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
#Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer(authProps.getIssuerUri())
.build();
}
#Bean
public TokenSettings tokenSettings() {
return TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofDays(1))
.refreshTokenTimeToLive(Duration.ofDays(1))
.build();
}
}
But am still facing the same issue.
How do I solve this? Any assistance is highly appreciated.
After trying out different solutions this was how I was able to solve it.
I changed my OAuth2AuthorizationService bean to look like this.
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService =
new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper oAuth2AuthorizationParametersMapper =
new JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper();
ObjectMapper objectMapper = new ObjectMapper();
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
List<Module> securityModules = SecurityJackson2Modules.getModules(classLoader);
objectMapper.registerModules(securityModules);
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
objectMapper.addMixIn(JwtAuthenticationToken.class, JwtAuthenticationTokenMixin.class);
rowMapper.setObjectMapper(objectMapper);
oAuth2AuthorizationParametersMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
authorizationService.setAuthorizationParametersMapper(oAuth2AuthorizationParametersMapper);
return authorizationService;
}
and here is my JwtAuthenticationTokenMixin configurations
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
#JsonDeserialize(using = JwtAuthenticationTokenDeserializer.class)
#JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
#JsonIgnoreProperties(ignoreUnknown = true)
public abstract class JwtAuthenticationTokenMixin {}
class JwtAuthenticationTokenDeserializer extends JsonDeserializer<JwtAuthenticationToken> {
#Override
public JwtAuthenticationToken deserialize(JsonParser parser, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JsonNode root = mapper.readTree(parser);
return deserialize(parser, mapper, root);
}
private JwtAuthenticationToken deserialize(JsonParser parser, ObjectMapper mapper, JsonNode root)
throws JsonParseException {
JsonNode principal = JsonNodeUtils.findObjectNode(root, "principal");
if (!Objects.isNull(principal)) {
String tokenValue = principal.get("tokenValue").textValue();
long issuedAt = principal.get("issuedAt").longValue();
long expiresAt = principal.get("expiresAt").longValue();
Map<String, Object> headers = JsonNodeUtils.findValue(
principal, "headers", JsonNodeUtils.STRING_OBJECT_MAP, mapper);
Map<String, Object> claims = new HashMap<>();
claims.put("claims", principal.get("claims"));
Jwt jwt = new Jwt(tokenValue, Instant.ofEpochMilli(issuedAt), Instant.ofEpochMilli(expiresAt), headers, claims);
return new JwtAuthenticationToken(jwt);
}
return null;
}
}
abstract class JsonNodeUtils {
static final TypeReference<Set<String>> STRING_SET = new TypeReference<Set<String>>() {
};
static final TypeReference<Map<String, Object>> STRING_OBJECT_MAP = new TypeReference<Map<String, Object>>() {
};
static String findStringValue(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isTextual()) ? value.asText() : null;
}
static <T> T findValue(JsonNode jsonNode, String fieldName, TypeReference<T> valueTypeReference,
ObjectMapper mapper) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isContainerNode()) ? mapper.convertValue(value, valueTypeReference) : null;
}
static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) {
if (jsonNode == null) {
return null;
}
JsonNode value = jsonNode.findValue(fieldName);
return (value != null && value.isObject()) ? value : null;
}
}
you don't need to create a Mixin, because it's all ready created by authorization springboot module. juste
#Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModules(new CoreJackson2Module());
objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
rowMapper.setObjectMapper(objectMapper);
authorizationService.setAuthorizationRowMapper(rowMapper);
return authorizationService;
}
i think you miss this line and is where the token mixin is registered
objectMapper.registerModules(new CoreJackson2Module());

How to configure the default ObjectMapper used by Spring to use a custom deserializer when deserializing spring classes

I want to use my own custom deserializer in Spring's default ObjectMapper whenever I have a class of type OAuth2AccessToken. The interface is annotated with
JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)
and this is what it's using at the moment to deserialize but I want to use my own.
So far I have created my own custom deserializer
public class MyCustomDeserializer extends StdDeserializer<OAuth2AccessToken> {
public MyCustomDeserializer() {
super(OAuth2AccessToken.class);
}
#Override
public OAuth2AccessToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String tokenValue = null;
String tokenType = null;
String refreshToken = null;
Long expiresIn = null;
Set<String> scope = null;
Map<String, Object> additionalInformation = new LinkedHashMap<String, Object>();
while (jp.nextToken() != JsonToken.END_OBJECT) {
String name = jp.getCurrentName();
jp.nextToken();
if (OAuth2AccessToken.ACCESS_TOKEN.equals(name)) {
tokenValue = jp.getText();
} else if (OAuth2AccessToken.TOKEN_TYPE.equals(name)) {
tokenType = jp.getText();
} else if (OAuth2AccessToken.REFRESH_TOKEN.equals(name)) {
refreshToken = jp.getText();
} else if (OAuth2AccessToken.EXPIRES_IN.equals(name)) {
try {
expiresIn = jp.getLongValue();
} catch (JsonParseException e) {
expiresIn = Long.valueOf(jp.getText());
}
} else if (OAuth2AccessToken.SCOPE.equals(name)) {
scope = parseScope(jp);
} else {
additionalInformation.put(name, jp.readValueAs(Object.class));
}
}
DefaultOAuth2AccessToken accessToken = new DefaultOAuth2AccessToken(tokenValue);
accessToken.setTokenType(tokenType);
if (expiresIn != null) {
accessToken.setExpiration(new Date(System.currentTimeMillis() + (expiresIn * 1000)));
}
if (refreshToken != null) {
accessToken.setRefreshToken(new DefaultOAuth2RefreshToken(refreshToken));
}
accessToken.setScope(scope);
accessToken.setAdditionalInformation(additionalInformation);
return accessToken;
}
private Set<String> parseScope(JsonParser jp) throws JsonParseException, IOException {
Set<String> scope;
if (jp.getCurrentToken() == JsonToken.START_ARRAY) {
scope = new TreeSet<String>();
while (jp.nextToken() != JsonToken.END_ARRAY) {
scope.add(jp.getValueAsString());
}
} else {
String text = jp.getText();
scope = OAuth2Utils.parseParameterList(text);
}
return scope;
}
}
My own custom class by extending DefaultOAuth2AccessToken
#com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = MyCustomDeserializer.class)
public class MyCustomOAuth2AccessToken extends DefaultOAuth2AccessToken {
public MyCustomOAuth2AccessToken(String value) {
super(value);
}
public MyCustomOAuth2AccessToken(OAuth2AccessToken accessToken) {
super(accessToken);
}
}
and at the moment I am registering a bean of type Jackson2ObjectMapperBuilderCustomizer like this
#Bean
public Jackson2ObjectMapperBuilderCustomizer addCustomDeserialization() {
return new Jackson2ObjectMapperBuilderCustomizer() {
#Override
public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
SimpleModule m = new SimpleModule();
m.addDeserializer(OAuth2AccessToken.class, new MyCustomDeserializer());
jacksonObjectMapperBuilder.modules(m);
}
};
}
#Bean
public OAuth2ClientContext getOAuth2ClientContext() {
DefaultOAuth2ClientContext defaultOAuth2ClientContext = new DefaultOAuth2ClientContext();
defaultOAuth2ClientContext.setAccessToken(new MyCustomOAuth2AccessToken("test"));
return defaultOAuth2ClientContext;
}
You can simply annotate your deserialization classes with #JsonComponent.
The annotation allows us to expose an annotated class to be a Jackson serializer and/or deserializer without the need to add it to the ObjectMapper manually.
To configure ObjectMapper globally just create a bean of type Jackson2ObjectMapperBuilder and use deserializerByType method :
#Bean
public Jackson2ObjectMapperBuilder objectMapperBuilder() {
return new Jackson2ObjectMapperBuilder()
.deserializerByType(OAuth2AccessToken.class, new MyCustomDeserializer());
}
Reference of configuring ObjectMapper in SpringBoot can be found here.

Springboot locale with Rest request and Thymeleaf

So another application directs user to my server. The redirect is Post request (application/json) with value language in the JSON. How should I set the locale value in RestController? So that Thymeleaf could render the correct text.
Setting locale with LocaleContextHolder doesn't do the trick.
You should follow this guide here since Internationalization is a common task in spring-boot. In case if you need a short answer:
First configure a LocaleResolver in your Application.java:
#Bean(name = "localeResolver")
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(new Locale("tr", "TR"));
return slr;
}
Then again in your Application.java file configure a LocaleChangeInterceptor:
#Bean(name = "localeChangeInterceptor")
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang");
return lci;
}
And finally register your LocaleChangeInterceptor (also in Application.java):
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
Now if you send a request parameter named "lang" with your POST request spring will use its value to determine the desired locale and change it accordingly.
Ended up with the following solution:
WebMvcConfigurer has these
#Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver r = new SessionLocaleResolver();
r.setDefaultLocale(new Locale("jp"));
return r;
}
And in the controller I call this classes public method:
#Component
public class WebLanguage {
public void setLocale(HttpServletRequest request, HttpServletResponse response) {
if (!request.getParameterMap().containsKey("lang")) return;
LocaleResolver localeResolver = localeResolver(request);
localeResolver.setLocale(request, response, new Locale(request.getParameterMap().get("lang")[0]));
}
LocaleResolver localeResolver(HttpServletRequest request) {
return RequestContextUtils.getLocaleResolver(request);
}
}

Accept Strings and XML data with RestController

I want to create REST Server which accepts XML requests and plain text into different controllers. I tried to implement this:
#SpringBootApplication
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
..............
private BasicAuthenticationInterceptor basicAuthenticationInterceptor;
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> converter instanceof MappingJackson2XmlHttpMessageConverter);
converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
converters.add(new MappingJackson2XmlHttpMessageConverter(
((XmlMapper) createObjectMapper(Jackson2ObjectMapperBuilder.xml()))
.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)));
converters.add(new MappingJackson2HttpMessageConverter(createObjectMapper(Jackson2ObjectMapperBuilder.json())));
}
private ObjectMapper createObjectMapper(Jackson2ObjectMapperBuilder builder) {
builder.indentOutput(true);
builder.modules(new JaxbAnnotationModule());
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.defaultUseWrapper(false);
return builder.build();
}
#Autowired
public void setBasicAuthenticationInterceptor(BasicAuthenticationInterceptor basicAuthenticationInterceptor) {
this.basicAuthenticationInterceptor = basicAuthenticationInterceptor;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthenticationInterceptor);
}
}
Check for XML proper formatting:
#ControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
#Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
PaymentTransaction response;
if (ex.getMessage().contains("Required request body")) {
response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 350,
"Invalid XML message: No XML data received", "XML request parsing failed!");
} else {
response = new PaymentTransaction(PaymentTransaction.Response.failed_response, 351,
"Invalid XML message format", null);
}
return ResponseEntity.badRequest().body(response);
}
}
Controller Class:
#RestController()
public class HomeController {
#Autowired
public HomeController(Map<String, MessageProcessor> processors, Map<String, ReconcileProcessor> reconcileProcessors,
#Qualifier("defaultProcessor") MessageProcessor defaultProcessor,
AuthenticationService authenticationService, ClientRepository repository,
#Value("${request.limit}") int requestLimit) {
// Here I receive XML
}
#GetMapping(value = "/v1/*")
public String message() {
return "REST server";
}
#PostMapping(value = "/v1/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#PathVariable("token") String token,
#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
// Here I receive XML
}
#PostMapping(value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#RequestBody Map<String, String> keyValuePairs) {
// Here I receive key and value in request body
}
#PostMapping(value = "/v1/summary/by_date/{token}", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponses handleReconcile(#PathVariable("token") String token, #RequestBody Reconcile reconcile,
HttpServletRequest request) throws Exception {
// Here I receive XML
}
#ResponseStatus(value = HttpStatus.UNAUTHORIZED)
public static class UnauthorizedException extends RuntimeException {
UnauthorizedException(String message) {
super(message);
}
}
}
As you can see in some methods I receive XML and in other I receive String in form of key=value&.....
How I configure Spring to accept both types?
Also should I split the Rest controller into different files?
EDIT:
Sample XML request:
<?xml version="1.0" encoding="UTF-8"?>
<payment_transaction>
<transaction_type>authorize</transaction_type>
<transaction_id>2aeke4geaclv7ml80</transaction_id>
<amount>1000</amount>
<currency>USD</currency>
<card_number>22</card_number>
<shipping_address>
<first_name>Name</first_name>
</shipping_address>
</payment_transaction>
Sample XML response:
<?xml version="1.0" encoding="UTF-8"?>
<payment_response>
<transaction_type>authorize</transaction_type>
<status>approved</status>
<unique_id>5f7edd36689f03324f3ef531beacfaae</unique_id>
<transaction_id>asdsdlddea4sdaasdsdsa4dadasda</transaction_id>
<code>500</code>
<amount>101</amount>
<currency>EUR</currency>
</payment_response>
Sample Notification request:
uniqueid=23434&type=sale&status=33
Sample Notification response: It should return only HTTP status OK.
I use:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath />
</parent>
Java version: "10.0.2" 2018-07-17
About the XML generation I use:
#XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {
public enum Response {
failed_response, successful_response
}
#XmlElement(name = "transaction_type")
public String transactionType;
#XmlElement(name = "transaction_id")
public String transactionId;
#XmlElement(name = "usage")
POM Configuration: https://pastebin.com/zXqYhDH3
For Spring boot 2.0.4-RELEASE, it seems you don't have to do a lot.
I made this configuration:
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
FormHttpMessageConverter converter = new FormHttpMessageConverter();
//MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
//converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
converters.add(converter);
MappingJackson2HttpMessageConverter conv1 = new MappingJackson2HttpMessageConverter();
conv1.getObjectMapper().registerModule(new JaxbAnnotationModule());
converters.add(conv1);
MappingJackson2XmlHttpMessageConverter conv = new MappingJackson2XmlHttpMessageConverter();
// required by jaxb annotations
conv.getObjectMapper().registerModule(new JaxbAnnotationModule());
converters.add(conv);
}
}
I used about your DTO:
#XmlRootElement(name = "payment_transaction")
public class PaymentTransaction {
#XmlElement(name = "transaction_type")
public String transactionType;
#XmlElement(name = "transaction_id")
public String transactionId;
public String getTransactionType() {
return transactionType;
}
public void setTransactionType(String transactionType) {
this.transactionType = transactionType;
}
public String getTransactionId() {
return transactionId;
}
public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
#Override
public String toString() {
return "PaymentTransaction [transactionType=" + transactionType
+ ", transactionId=" + transactionId + "]";
}
}
The controller:
#RestController
public class MyController {
/**
* https://stackoverflow.com/questions/34782025/http-post-request-with-content-type-application-x-www-form-urlencoded-not-workin/38252762#38252762
*/
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody PaymentTransaction handleMessage(#PathVariable("token") String token,
#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
System.out.println("handleXmlMessage");
System.out.println(transaction);
PaymentTransaction body = new PaymentTransaction();
body.setTransactionId(transaction.getTransactionId());
body.setTransactionType("received: " + transaction.getTransactionType());
return body;
}
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute PaymentTransaction transaction) {
System.out.println("handleFormMessage");
System.out.println(transaction);
return new ResponseEntity<String>(HttpStatus.OK);
}
}
The only main thing to remember that it seems the filling of the DTO with the parsed data happens by reflection:
For your input
<payment_transaction>
<transaction_id>1</transaction_id>
<transaction_type>name</transaction_type>
</payment_transaction>
I got this response (see my controller):
{
"transactionType": "received: null",
"transactionId": null
}
But when I changed to the name of the fields of the DTO, it started to work (the root element did not matter, interesting):
<payment_transaction>
<transactionId>1</transactionId>
<transactionType>name</transactionType>
</payment_transaction>
result:
{
"transactionType": "received: name",
"transactionId": "1"
}
The same is true for the querystring. I don't know what to change to get spring to parse the xmls using the defined names in #XmlRootElement/#XmlElement.
This is an another solution (it worked well for me) with less Spring magic and using the good old way of HttpServletRequestWrapper.
In the WebMvcConfigurerAdapter class, now we don't need the MessageConverter:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
//FormHttpMessageConverter converter = new FormHttpMessageConverter();
//MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
//converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
//converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
//converters.add(converter);
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new MappingJackson2XmlHttpMessageConverter());
super.configureMessageConverters(converters);
}
And everything else happens in this (servlet) Filter implementation:
#WebFilter("/v1/notification")
public class MyRequestBodyFilter implements Filter {
private static class MyServletInputStream extends ServletInputStream {
private ByteArrayInputStream buffer;
public MyServletInputStream(byte[] contents) {
this.buffer = new ByteArrayInputStream(contents);
}
#Override
public int read() throws IOException {
return buffer.read();
}
#Override
public boolean isFinished() {
return buffer.available() == 0;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper{
MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
#Override
public ServletInputStream getInputStream() throws IOException {
// converting the request parameters to the pojo and serialize it to XML
// the drawback of this way that the xml will be parsed again somewhere later
long id = Long.parseLong(getRequest().getParameter("id"));
String name = getRequest().getParameter("name");
MyRequestBody body = new MyRequestBody();
body.setId(id);
body.setName(name);
return new MyServletInputStream(new XmlMapper().writeValueAsBytes(body));
}
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
chain.doFilter(new MyHttpServletRequestWrapper(httpRequest), response);
}
#Override
public void destroy() {
}
}
I have changed nothing in my test controller, so the signature of the methods remained the same:
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody MyResponseBody handleMessage(#PathVariable("token") String token, #RequestBody MyRequestBody transaction, HttpServletRequest request) throws Exception {
MyResponseBody body = new MyResponseBody();
body.setId(transaction.getId());
body.setName("received " + transaction.getName());
return body;
}
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute MyRequestBody transaction) {
return new ResponseEntity<String>(HttpStatus.OK);
}
Update this solution works for pre-2.x Spring-boot versions. Another thing to consider that during my tests I used Jackson's XML annotations on my DTOs (JacksonXmlRootElement, JacksonXmlProperty) and maybe FormHttpMessageConverter can handle DTOs with standard JAXB annotations (see my answer for Spring 2.0.4-RELEASE) - so may you'd better to go to that direction if you can (or at least give it a try before you apply the sketched solution).
This is my solution. I dropped the RequestIntereptor (because that is rather for inspect the request not for modifying it) and the RequestBodyAdvice too (because it turned out that there is a better way.
If you have a look for the available MessageConverters you can see that the only MessageConverter that reads the posted form data is the FormHttpMessageConverter.
The problem with this class is the return type, which is Multivaluemap
But, using this class as a base, I have created an abstract class that reads the form data to this Multivaluemap, and have only one abstract funtion that you have to implement in the subclass: that will create an object from the values stored in the multivaluemap.
Unfortunately I had to introduce an interface (because I kept the original implementation of the writing part just adopt it) on the DTO you would like to read.
All in all, my working solution:
In the WebMvcConfigurerAdapter class, I have this config:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
MyRequestBodyHttpMessageConverter converter = new MyRequestBodyHttpMessageConverter();
//FormHttpMessageConverter converter = new FormHttpMessageConverter();
MediaType utf8FormEncoded = new MediaType("application","x-www-form-urlencoded", Charset.forName("UTF-8"));
//MediaType mediaType = MediaType.APPLICATION_FORM_URLENCODED; maybe UTF-8 is not needed
//converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED));
converter.setSupportedMediaTypes(Arrays.asList(utf8FormEncoded));
converters.add(converter);
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new MappingJackson2XmlHttpMessageConverter());
super.configureMessageConverters(converters);
}
I modified a bit your controller functions:
#PostMapping(value = "/v1/{token}",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody MyResponseBody handleMessage(#PathVariable("token") String token, #RequestBody MyRequestBody transaction, HttpServletRequest request) throws Exception {
MyResponseBody body = new MyResponseBody();
body.setId(transaction.getId());
body.setName("received " + transaction.getName());
return body;
}
// check #ModelAttribute workaround https://stackoverflow.com/questions/4339207/http-post-with-request-content-type-form-not-working-in-spring-mvc-3
#PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, value = "/v1/notification")
public ResponseEntity<String> handleNotifications(#ModelAttribute MyRequestBody transaction) {
return new ResponseEntity<String>(HttpStatus.OK);
}
(in the next part the import packages are meaningful, some mail api classes can be found somewhere else)
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.mail.internet.MimeUtility;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.StreamingHttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* based on {#link org.springframework.http.converter.FormHttpMessageConverter
*
* it uses the readed MultiValueMap to build up the DTO we would like to get from the request body.
*/
public abstract class AbstractRequestBodyFormHttpMessageConverter<T extends RequestParamSupport> implements HttpMessageConverter<T> {
/**
* This is the only method you have to implement for your DTO class
* the class must implement RequestParamSupport
*/
protected abstract T buildObject(MultiValueMap<String, Object> valueMap);
public interface RequestParamSupport{
MultiValueMap<String, Object> getRequestParams();
}
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
private Charset charset = DEFAULT_CHARSET;
private Charset multipartCharset;
private Class<T> bodyClass;
public AbstractRequestBodyFormHttpMessageConverter(Class<T> bodyClass) {
this.bodyClass = bodyClass;
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.partConverters.add(new ByteArrayHttpMessageConverter());
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
applyDefaultCharset();
}
/**
* Set the character set to use when writing multipart data to encode file
* names. Encoding is based on the encoded-word syntax defined in RFC 2047
* and relies on {#code MimeUtility} from "javax.mail".
* <p>If not set file names will be encoded as US-ASCII.
* #since 4.1.1
* #see Encoded-Word
*/
public void setMultipartCharset(Charset charset) {
this.multipartCharset = charset;
}
/**
* Apply the configured charset as a default to registered part converters.
*/
private void applyDefaultCharset() {
for (HttpMessageConverter<?> candidate : this.partConverters) {
if (candidate instanceof AbstractHttpMessageConverter) {
AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
// Only override default charset if the converter operates with a charset to begin with...
if (converter.getDefaultCharset() != null) {
converter.setDefaultCharset(this.charset);
}
}
}
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
if (!bodyClass.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
// We can't read multipart....
if (!supportedMediaType.equals(MediaType.MULTIPART_FORM_DATA) && supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
if (!bodyClass.isAssignableFrom(clazz)) {
return false;
}
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
/**
* Set the list of {#link MediaType} objects supported by this converter.
*/
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) {
this.supportedMediaTypes = supportedMediaTypes;
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(this.supportedMediaTypes);
}
#Override
public T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, Object> result = new LinkedMultiValueMap<String, Object>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
}
else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
return buildObject(result);
}
#Override
public void write(T object, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException,
HttpMessageNotWritableException {
if (!isMultipart(object, contentType)) {
writeForm(object.getRequestParams(), contentType, outputMessage);
}
else {
writeMultipart(object.getRequestParams(), outputMessage);
}
}
private boolean isMultipart(RequestParamSupport object, MediaType contentType) {
if (contentType != null) {
return MediaType.MULTIPART_FORM_DATA.includes(contentType);
}
MultiValueMap<String, Object> map = object.getRequestParams();
for (String name : map.keySet()) {
for (Object value : map.get(name)) {
if (value != null && !(value instanceof String)) {
return true;
}
}
}
return false;
}
private void writeForm(MultiValueMap<String, Object> form, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
Charset charset;
if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType);
charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
}
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
charset = this.charset;
}
StringBuilder builder = new StringBuilder();
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
String name = nameIterator.next();
for (Iterator<Object> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
String value = (String) valueIterator.next();
builder.append(URLEncoder.encode(name, charset.name()));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(value, charset.name()));
if (valueIterator.hasNext()) {
builder.append('&');
}
}
}
if (nameIterator.hasNext()) {
builder.append('&');
}
}
final byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
#Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(bytes, outputStream);
}
});
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
final byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
#Override
public void writeTo(OutputStream outputStream) throws IOException {
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
}
});
}
else {
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
}
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
}
}
}
}
#SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
}
/**
* Generate a multipart boundary.
* <p>This implementation delegates to
* {#link MimeTypeUtils#generateMultipartBoundary()}.
*/
protected byte[] generateMultipartBoundary() {
return MimeTypeUtils.generateMultipartBoundary();
}
/**
* Return an {#link HttpEntity} for the given part Object.
* #param part the part to return an {#link HttpEntity} for
* #return the part Object itself it is an {#link HttpEntity},
* or a newly built {#link HttpEntity} wrapper for that part
*/
protected HttpEntity<?> getHttpEntity(Object part) {
return (part instanceof HttpEntity ? (HttpEntity<?>) part : new HttpEntity<Object>(part));
}
/**
* Return the filename of the given multipart part. This value will be used for the
* {#code Content-Disposition} header.
* <p>The default implementation returns {#link Resource#getFilename()} if the part is a
* {#code Resource}, and {#code null} in other cases. Can be overridden in subclasses.
* #param part the part to determine the file name for
* #return the filename, or {#code null} if not known
*/
protected String getFilename(Object part) {
if (part instanceof Resource) {
Resource resource = (Resource) part;
String filename = resource.getFilename();
if (filename != null && this.multipartCharset != null) {
filename = MimeDelegate.encode(filename, this.multipartCharset.name());
}
return filename;
}
else {
return null;
}
}
private void writeBoundary(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
writeNewLine(os);
}
private static void writeEnd(OutputStream os, byte[] boundary) throws IOException {
os.write('-');
os.write('-');
os.write(boundary);
os.write('-');
os.write('-');
writeNewLine(os);
}
private static void writeNewLine(OutputStream os) throws IOException {
os.write('\r');
os.write('\n');
}
/**
* Implementation of {#link org.springframework.http.HttpOutputMessage} used
* to write a MIME multipart.
*/
private static class MultipartHttpOutputMessage implements HttpOutputMessage {
private final OutputStream outputStream;
private final HttpHeaders headers = new HttpHeaders();
private boolean headersWritten = false;
public MultipartHttpOutputMessage(OutputStream outputStream) {
this.outputStream = outputStream;
}
#Override
public HttpHeaders getHeaders() {
return (this.headersWritten ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers);
}
#Override
public OutputStream getBody() throws IOException {
writeHeaders();
return this.outputStream;
}
private void writeHeaders() throws IOException {
if (!this.headersWritten) {
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
byte[] headerName = getAsciiBytes(entry.getKey());
for (String headerValueString : entry.getValue()) {
byte[] headerValue = getAsciiBytes(headerValueString);
this.outputStream.write(headerName);
this.outputStream.write(':');
this.outputStream.write(' ');
this.outputStream.write(headerValue);
writeNewLine(this.outputStream);
}
}
writeNewLine(this.outputStream);
this.headersWritten = true;
}
}
private byte[] getAsciiBytes(String name) {
try {
return name.getBytes("US-ASCII");
}
catch (UnsupportedEncodingException ex) {
// Should not happen - US-ASCII is always supported.
throw new IllegalStateException(ex);
}
}
}
/**
* Inner class to avoid a hard dependency on the JavaMail API.
*/
private static class MimeDelegate {
public static String encode(String value, String charset) {
try {
return MimeUtility.encodeText(value, charset, null);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}
The bean converter implementation
public class MyRequestBodyHttpMessageConverter extends
AbstractRequestBodyFormHttpMessageConverter<MyRequestBody> {
public MyRequestBodyHttpMessageConverter() {
super(MyRequestBody.class);
}
#Override
protected MyRequestBody buildObject(MultiValueMap<String, Object> valueMap) {
MyRequestBody parsed = new MyRequestBody();
parsed.setId(Long.valueOf((String)valueMap.get("id").get(0)));
parsed.setName((String)valueMap.get("name").get(0));
parsed.setRequestParams(valueMap);
return parsed;
}
}
And finally the MyRequestBody DTO (the MyRequestBody was the same just with different name)
#JacksonXmlRootElement
public class MyRequestBody implements RequestParamSupport, Serializable {
#JsonIgnore
private transient MultiValueMap<String, Object> requestParams;
#JacksonXmlProperty
private Long id;
#JacksonXmlProperty
private String name;
//empty constructor, getters, setters, tostring, etc
#Override
public MultiValueMap<String, Object> getRequestParams() {
return requestParams;
}
}
** Finally my answers: **
How I configure Spring to accept both types?
As you can see, you have to have your own form-data to your bean converter.
(Do not forget that you have to use #ModelAttribute when you are mapping from form data and not #RequestBody.)
Also should I split the Rest controller into different files?
No, that is not necessary, just register your converter.

Resources