how to apply spring message convertors based on condition? - java

I have a controller whose response is camelCase json value. Now we are re-writing the code with new version and the response required is in snake_case.
I have added a message converter and modified object mapper to setsetPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
public class ResponseJSONConverter extends MappingJackson2HttpMessageConverter {
#Autowired
public ResponseJSONConverter(ObjectMapper objectMapper) {
setObjectMapper(objectMapper);
}
}
I have registered this convertor with spring and its working as expected. Now I want my old endpoints to return in camelCase for backward compatibility for my consumers and new endpoints with snake_case.
I have tried to have one more message convertor with simple object mapper without setting camelCase to Snake case property and registered with spring. Only one message convertor gets applied based on the order declared in the spring configuration.
Is there any way we can achieve this ? Loading message convertor based on the condition ?
EDIT
Added my spring config file
<beans xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<bean id="moneySerializer" class="api.serialize.MoneySerializer"/>
<bean id="moneyDeserializer" class="api.serialize.MoneyDeserializer"/>
<bean id="serializationModule" class="api.serialize.SerializationModule">
<constructor-arg index="0" ref="moneySerializer"/>
<constructor-arg index="1" ref="moneyDeserializer"/>
</bean>
<bean id="customObjectMapper" class="api.serialize.CustomObjectMapper" primary="true">
<constructor-arg ref="serializationModule"/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="api.serialize.ResponseJSONConverterCamelCaseToSnakeCase" >
<constructor-arg ref="customObjectMapper"/>
</bean>
<bean class="api.serialize.ResponseJSONConverter">
<constructor-arg ref="objectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper"/>
</beans>
EDIT 2.0
my servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.tgt.promotions.api.serialize.ServiceJSONConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
CustomMessageConverter
public class ServiceJSONConverter extends MappingJackson2HttpMessageConverter {
#Autowired
public ServiceJSONConverter(SnakeCaseObjectMapper snakeCaseObjectMapper) {
setObjectMapper(snakeCaseObjectMapper);
}
}
Custom Object Mapper
#Component
public class SnakeCaseObjectMapper extends ObjectMapper {
#Autowired
public SnakeCaseObjectMapper(PropertyNamingStrategy propertyNamingStrategy) {
setSerializationInclusion(JsonInclude.Include.NON_NULL);
setPropertyNamingStrategy(propertyNamingStrategy);
}
}
Custom Property Naming Strategy
#Component
public class CustomPropertyNamingStrategy extends PropertyNamingStrategy {
#Autowired
private HttpServletRequest request;
private final PropertyNamingStrategy legacyStrategy = PropertyNamingStrategy.LOWER_CASE;
private final PropertyNamingStrategy defaultStrategy = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;
#Override
public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
return getStrategy().nameForConstructorParameter(config, ctorParam, defaultName);
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
return getStrategy().nameForField(config, field, defaultName);
}
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return getStrategy().nameForGetterMethod(config, method, defaultName);
}
#Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return getStrategy().nameForSetterMethod(config, method, defaultName);
}
private PropertyNamingStrategy getStrategy() {
if (isLegacyEndpoint(request)) {
return legacyStrategy;
} else {
return defaultStrategy;
}
}
private boolean isLegacyEndpoint(HttpServletRequest request) {
return request != null && request.getRequestURL() != null && !request.getRequestURL().toString().contains("/v3");
}
}

Instead of having 2 different object-mappers, I suggest creating a custom implementation of PropertyNamingStrategy, using the 2 other strategies accordingly:
public class AwesomePropertyNamingStrategy extends PropertyNamingStrategy {
private PropertyNamingStrategy legacyStrategy = PropertyNamingStrategy.LOWER_CASE;
private PropertyNamingStrategy defaultStrategy = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES;
#Override
public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
return getStrategy().nameForConstructorParameter(config, ctorParam, defaultName);
}
// TODO: implement other nameForXXX methods
private PropertyNamingStrategy getStrategy() {
if (isLegacyEndpoint()) {
return legacyStrategy;
} else {
return defaultStrategy;
}
}
private boolean isLegacyEndpoint() {
// TODO: get hold of the RequestContext or some other thead-local context
// that allows you to know it's an old or a new endpoint
return false;
}
}
You should come up with a way to toggle between legacy and new mode:
Using the endpoint URL by accessing the request context in some way
In case your old endpoint use different response objects, use the class of the object that is being converted to determine legacy/normal or your own custom #LegacyResponse annotation on all old classes instead.

Well, nothing worked after many attempts. Finally ended up defining 2 different servlets. one without any version and one with v1 version.
web.xml
<servlet>
<servlet-name>snake-case</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>snake-case</servlet-name>
<url-pattern>/v1</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>camel-case</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>camel-case</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Accordingly defined two servlets snake-case-servlet.xml and camel-case-servlet.xml.
snake-case-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.tgt.promotions.api.serialize.DataJSONConverter">
<constructor-arg ref="snakeCaseObjectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
camel-case-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean class="com.tgt.promotions.api.serialize.DataJSONConverter">
<constructor-arg ref="objectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
Now, for any requests with /v1* , snakeCaseObjectMapper is used and for other requests default object mapper is used.

Related

Spring Inject boolean value from property file using XML is always false

I am trying to inject boolean property from property file. the value of the attribute is alway false
the property
use.virtual.wallet=true
The xml configuration
<bean id="proxyUtil" class="com.util.ProxyServiceUtility">
<property name="useVirtualWallet" value="${use.virtual.wallet}" />
</bean>
the bean
public class ProxyServiceUtility {
private boolean useVirtualWallet;
public void setUseVirtualWallet(boolean useVirtualWallet) {
this.useVirtualWallet = useVirtualWallet;
}
public boolean isUseVirtualWallet() {
return useVirtualWallet;
}
}
useVirtualWallet is alway false
You have to load your properties file into Spring context using PropertyPlaceholderConfigurer.
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations" value="classpath:com/foo/jdbc.properties"/>
</bean>
The problem fixed using this workaround, instead of injecting boolean, I injected String and then converted that String to boolean on the setter
public void setUseVirtualWallet(String useVirtualWallet) {
this.useVirtualWallet = Boolean.parseBoolean(useVirtualWallet);
}
Another variant
<beans
xmlns:context="http://www.springframework.org/schema/context">
<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>
...
<beans>

#PreAuthorize is not invoked

I configured my security.xml file to support preAuthorized annotations
<bean id="permissionEvaluator" class="package.MyPermissionEvaluatorClass" />
<bean id="expressionHandler"
class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
<property name="permissionEvaluator" ref="permissionEvaluator" />
</bean>
<global-method-security pre-post-annotations="enabled" proxy-target-class="true">
<expression-handler ref="expressionHandler"/>
</global-method-security>
I added proxy-target-class=true as my REST class
My REST method with annotation looks like this
#PreAuthorize("#permissionEvaluator.hasPermission(#organizationId, 'MY_PERMISSION')")
#RequestMapping()
public ModelAndView findProductConfigurationsByOrganizationId(...) {}
My permissions evaluation class looks like this
public class MyPermissionEvaluatorClass extends PermissionEvaluatorImpl {
#Override
public boolean hasPermission(Authentication userObj, Object target, Object permission) {
LOGGER.info("Attempt to resolve permissions for {}.", target);
return false;
}
#Override
public boolean hasPermission(Authentication userObj, Serializable target, String targetType,
Object permission) {
return false;
}
But when I do rest requests - it seems does not enter method hasPermission at all.
What is the problem?

Spring Dependency injection with factory(dynamic value)

I'm new to spring.
I've a rulefactory, which would return an instance from a static method
based on the type value
Now i'll get the type from the main methods, argument.
Now i would like to pass the argument type to the factory method getInstance
type argument.
how to do that.
/* Factory class, getInstance will return a subtype of RuleEvaluation, for simplicity, i've not
provided the Implementation class for SingleRuleEvaluation and MassRuleEvaluation. Basically both the classes implements RuleEvaluation */
public class RuleEvalFactory {
public static RuleEvaluation getInstance(String type) {
if (type != null && type.equals("Single")) {
return new SingleRuleEvaluation();
} else if (type != null && type.equals("mass")) {
return new MassRuleEvaluation();
}
return null;
}
}
/* My Main class , i need to get an instance of the RuleEvaluation here based on the type(dyamic)
dont know how to do it.
*/
public class MyApp {
public static void main(String args[]) {
ApplicationContext context =
new ClassPathXmlApplicationContext("Spring-All-Module.xml");
String type = args[0];
/* i want to pass the above type String to the factory method and get the instance how to do that */
RuleEvaluation re = (HarmonyService) context.getBean("rulefactory") ;
}
}
/* my Spring xml configuration file */
Spring xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<bean id="instanceMethodFactory" class="test.factory.RuleEvalFactory"> </bean>
<!-- i dont know how to pass the dynamic type from the Myapp main
method into this constructory argument -->
<bean id="rulefactory" factory-bean="instanceMethodFactory" factory-method="getInstance">
<constructor-arg index="0"> </constructor-arg>
</bean>
</beans>
Please give the code in Spring xml and Myapp main method how to inject the type into the factory method's getInstance.
Regards,
Raghu
You need to specify in the bean the constructor argument,
<bean id="myBean" class="A" scope="prototype">
<constructor-arg value="0"/> <!-- dummy value -->
</bean>
And then pass the value to bean factory,
getBean("myBean", argument);

REST+Spring+POST with custom marshaller

I am developing RESTful API for my application. All getters (that use HTTP GET) work fine. I cannot make save method (that uses POST) to work.
I am using HTML form and RESTClient for testing.
Here is my Controller
#Controller
public class EntitiesController {
#RequestMapping(value="/ci/save/", method = RequestMethod.POST)
public ModelAndView saveConfigurationItem(#RequestBody ConfigurationItem body) {
System.out.println("saveConfigurationItem: body=" + body);
return createModelAndView("ci", Collections.emptyList());
}
}
This method is expected to be called when client posts ConfigurationItem.
I am using custom serialization format. It is not XML or JSON. It is VCard or VCalendar format. For my first test I used the following VCard:
BEGIN:VCARD
N:Pooh;Winnie
FN:Winnie the Pooh
TEL:tel:+441234567
END:VCARD
I posted it to URL http://localhost:8080/core.solution-1.0/data/ci/save/.
Here is the response I get:
415
The server refused this request because the request entity is in a format not
supported by the requested resource for the requested method ()
(*) ConfigurationItem is an abstract class. CardEntry extends it. I tried both.
I tried to change the method parameter to String. In this case the method is called but the string is empty. The same happens when following one of recommendations I saw in web I changed the parameter type to MultiValueMap and sent request from simple HTML form.
I saw that marshal() is not called at all.
What's wrong?
Here is what I have. (I put here relevant code only.)
Spring configuration
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:oxm="http://www.springframework.org/schema/oxm"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-3.0.xsd">
<import resource="classes/spring-config-prod.xml"/>
<context:component-scan base-package="com.mycompany.solution.service" />
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
<bean id="ciCardView" class="com.mycompany.solution.service.VFormatView">
<constructor-arg>
<bean class="com.mycompany.solution.service.VFormatMarshaller">
<property name="packagesToScan" value="com.mycompany.solution.entity"/>
</bean>
</constructor-arg>
</bean>
</beans>
Marshaller
public class VFormatMarshaller implements Marshaller, Unmarshaller {
#Override
public void marshal(Object obj, Result result)
throws IOException/*, XmlMappingException*/ {
System.out.println("VFormatMarshaller.marshal(" + obj + ")");
marshalStreamResult(obj, (StreamResult)result);
}
#Override
public boolean supports(Class<?> paramClass) {
System.out.println("VFormatMarshaller.supports(" + paramClass + ")");
boolean supports = new HashSet<String>(Arrays.asList(packagesToScan)).contains(paramClass.getPackage().getName());
if (supports) {
return supports;
}
return Collection.class.isAssignableFrom(paramClass);
}
#Override
public Object unmarshal(Source source) throws IOException/*, XmlMappingException*/ {
System.out.println("VFormatMarshaller.unmarshal(" + source + ")");
return unmarshalStreamSource((StreamSource)source);
}
//// .............................
}
View (this is written only to override the content type)
public class VFormatView extends MarshallingView {
public VFormatView() {
super();
setContentType("application/vcard");
System.out.println("VFormatView()");
}
public VFormatView(Marshaller marshaller) {
super(marshaller);
setContentType("application/vcard");
System.out.println("VFormatView(" + marshaller + ")");
}
}
#RequestBody/#ResponseBody are supported by an hierarchy of HttpMessageConverters, that is completely different from ViewResolvers.
In you case you need to configure a MarshallingHttpMessageConverter with appropriate marshaller/unmarshaller and content type (or create your own HttpMessageConverter if you don't need to depend on the existing implementation of marshaller/unmarshaller), and supply a configured instance to AnnotationMethodHandlerAdapter.
The least intrusive way to configure a custom HttpMessageConveter is to create a BeanPostProcessor as follows:
public class Configurer implements BeanPostProcessor {
public void postProcessAfterInitialization(String name, Object bean) {
if (bean instanceof AnnotationMethodHandlerAdapter) {
AnnotationMethodHandlerAdapter a = (AnnotationMethodHandlerAdapter) bean;
HttpMessageConverter[] convs = a.getMessageConverters();
... add new converter ...
a.setMessageConverters(convs);
}
}
...
}

annotations in Spring MVC

I'd like to convert this SimpleFormController to use the annotation support introduced in Spring MVC 2.5
Java
public class PriceIncreaseFormController extends SimpleFormController {
ProductManager productManager = new ProductManager();
#Override
public ModelAndView onSubmit(Object command)
throws ServletException {
int increase = ((PriceIncrease) command).getPercentage();
productManager.increasePrice(increase);
return new ModelAndView(new RedirectView(getSuccessView()));
}
#Override
protected Object formBackingObject(HttpServletRequest request)
throws ServletException {
PriceIncrease priceIncrease = new PriceIncrease();
priceIncrease.setPercentage(20);
return priceIncrease;
}
}
Spring Config
<!-- Include basic annotation support -->
<context:annotation-config/>
<!-- Comma-separated list of packages to search for annotated controllers. Append '.*' to search all sub-packages -->
<context:component-scan base-package="springapp.web"/>
<!-- Enables use of annotations on controller methods to map URLs to methods and request params to method arguments -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
<bean name="/priceincrease.htm" class="springapp.web.PriceIncreaseFormController">
<property name="sessionForm" value="true"/>
<property name="commandName" value="priceIncrease"/>
<property name="commandClass" value="springapp.service.PriceIncrease"/>
<property name="validator">
<bean class="springapp.service.PriceIncreaseValidator"/>
</property>
<property name="formView" value="priceincrease"/>
<property name="successView" value="hello.htm"/>
<property name="productManager" ref="productManager"/>
</bean>
Basically, I'd like to replace all the XML configuration for the /priceincrease.htm bean with annotations within the Java class. Is this possible, and if so, what are the corresponding annotations that I should use?
Thanks,
Don
It'll look something like the following, although whether it works or not exactly as is will depend a bit on your configuration (view resolver, etc). I should also note that there are about eight billion valid ways to write this thing. See the Spring documentation, 13.11.4 "Supported handler method arguments and return types" for an overview of the insanity. Also note that you can autowire the validator
#Controller
#RequestMapping("/priceincrease.htm")
public class PriceIncreaseFormController {
ProductManager productManager;
#Autowired
public PriceIncreaseFormController(ProductManager productManager) {
this.productManager = productManager;
}
// note: this method does not have to be called onSubmit
#RequestMapping(method = RequestMethod.POST)
public String onSubmit(#ModelAttribute("priceIncrease") PriceIncrease priceIncrease, BindingResult result, SessionStatus status {
new PriceIncreaseValidator().validate(priceIncrease, result);
if (result.hasErrors()) {
return "priceincrease";
}
else {
int increase = priceIncrease.getPercentage();
productManager.increasePrice(increase);
status.setComplete();
return "redirect:hello.htm";
}
}
// note: this method does not have to be called setupForm
#RequestMapping(method = RequestMethod.GET)
public String setupForm(Model model) {
PriceIncrease priceIncrease = new PriceIncrease();
priceIncrease.setPercentage(20);
model.addAttribute("priceIncrease", priceIncrease);
return "priceincrease";
}
}
Someone completed this project with a recent MVC and it's on github, so you can see how all the classes are changed compared to Spring's tutorial.
Link: PriceIncreaseFormController

Categories