custom param convertors in spring - spring

Is there any custom param convertors in Spring.For example
#RequestMapping(value="/getEmployees/{"empName"}")
public void getEmployees(#PathVariable("empName") Employee employee)
{
}
The path variable which is getting from request is of type spring.Based on the given empName it will assign to Employee object.In JAX-RS we can use ParamConvertor and ParamConvertorProvider to convert.Like in JAX-RS do we have any convertors?

Yes, you can use custom params convertors in Spring
1 Create a converter component that implements spring Converter interface
#Component
public class StringToRightsModeConverter implements Converter<String, RightsMode> {
#Override
public RightsMode convert(String s) {
try{
return RightsMode.valueOf(s);
} catch (Exception e) {
return RightsMode.getByCode(s);
}
}
}
2 That's all. Spring will automatically use it for String -> RightsMode type conversion
#GetMapping({"/periods"})
public List<Period> periods(
#RequestParam(required = false) RightsMode rightsMode) {
................................................................
}
Spring 3 type conversion

Related

Dozer seeks xml configs instead of Java confiigs

I am working on a Spring Boot project with Spring Data Rest, Gradle and Oracle Express DB in which I use DozerBeanMapper to map entities to DTOs and vice versa, I use no xml configurations for Dozer, just Java ones:
#Slf4j
#Configuration
public class DozerConfig {
#Bean
public DozerBeanMapper getDozerMapper() {
log.info("Initializing DozerBeanMapper bean");
return new DozerBeanMapper();
}
}
Also for clarity I have explicitly configured all the fields that has to be mapped although they are all with the same names. For example my Client mapper:
#Slf4j
#Component
public class ClientMapper extends BaseMapper {
private BeanMappingBuilder builder = new BeanMappingBuilder() {
#Override
protected void configure() {
mapping(Client.class, ClientDTO.class)
.fields("id", "id")
.fields("name", "name")
.fields("midName", "midName")
.fields("surname", "surname")
.exclude("password")
.fields("phone", "phone")
.fields("email", "email")
.fields("address", "address")
.fields("idCardNumber", "idCardNumber")
.fields("idCardIssueDate", "idCardIssueDate")
.fields("idCardExpirationDate", "idCardExpirationDate")
.fields("bankAccounts", "bankAccounts")
.fields("accountManager", "accountManager")
.fields("debitCardNumber", "debitCardNumber")
.fields("creditCardNumber", "creditCardNumber")
.fields("dateCreated", "dateCreated")
.fields("dateUpdated", "dateUpdated");
}
};
#Autowired
public ClientMapper(DozerBeanMapper mapper) {
super(mapper);
mapper.addMapping(builder);
}
public ClientDTO toDto(Client entity) {
log.info("Mapping Client entity to DTO");
return mapper.map(entity, ClientDTO.class);
}
public Client toEntity(ClientDTO dto) {
log.info("Mapping Client DTO to entity");
return mapper.map(dto, Client.class);
}
public List<ClientDTO> toDtos(List<Client> entities) {
log.info("Mapping Client entities to DTOs");
return entities.stream()
.map(entity -> toDto(entity))
.collect(Collectors.toList());
}
public List<Client> toEntities(List<ClientDTO> dtos) {
log.info("Mapping Client DTOs to entities");
return dtos.stream()
.map(dto -> toEntity(dto))
.collect(Collectors.toList());
}
public EmployeeDTO toEmployeeDto(Employee entity) {
log.info("Mapping Employee entity to DTO");
return mapper.map(entity, EmployeeDTO.class);
}
public Employee toEmployeeEntity(EmployeeDTO dto) {
log.info("Mapping Employee DTO to entity");
return mapper.map(dto, Employee.class);
}
public List<EmployeeDTO> toEmployeeDtos(List<Employee> entities) {
log.info("Mapping Employee entities to DTOs");
return entities.stream()
.map(entity -> toEmployeeDto(entity))
.collect(Collectors.toList());
}
public List<Employee> toEmployeeEntities(List<EmployeeDTO> dtos) {
log.info("Mapping Employee DTOs to entities");
return dtos.stream()
.map(dto -> toEmployeeEntity(dto))
.collect(Collectors.toList());
}
}
Despite this I get the following exception:
"httpStatus": "500 Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"message": "setAttribute(name, value):\n name: "http://apache.org/xml/features/validation/schema\"\n value: \"true\"",
"stackTrace": [
"oracle.xml.jaxp.JXDocumentBuilderFactory.setAttribute(JXDocumentBuilderFactory.java:289)",
"org.dozer.loader.xml.XMLParserFactory.createDocumentBuilderFactory(XMLParserFactory.java:71)",
"org.dozer.loader.xml.XMLParserFactory.createParser(XMLParserFactory.java:50)",
"org.dozer.loader.xml.MappingStreamReader.<init>(MappingStreamReader.java:43)",
"org.dozer.loader.xml.MappingFileReader.<init>(MappingFileReader.java:44)",
"org.dozer.DozerBeanMapper.loadFromFiles(DozerBeanMapper.java:219)",
"org.dozer.DozerBeanMapper.loadCustomMappings(DozerBeanMapper.java:209)",
"org.dozer.DozerBeanMapper.initMappings(DozerBeanMapper.java:315)",
"org.dozer.DozerBeanMapper.getMappingProcessor(DozerBeanMapper.java:192)",
"org.dozer.DozerBeanMapper.map(DozerBeanMapper.java:120)",
"com.rosenhristov.bank.exception.mapper.ClientMapper.toDto(ClientMapper.java:52)",
"com.rosenhristov.bank.service.ClientService.lambda$getClientById$0(ClientService.java:27)",
"java.base/java.util.Optional.map(Optional.java:265)",
"com.rosenhristov.bank.service.ClientService.getClientById(ClientService.java:27)",
"com.rosenhristov.bank.controller.ClientController.getClientById(ClientController.java:57)",
"java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)",
"java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)",
"java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)",
"java.base/java.lang.reflect.Method.invoke(Method.java:566)",
"org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197)",
"org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141)",
"org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)",
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:893)",
"org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:807)",
"org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)",
"org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1061)",
"org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:961)",
"org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)",
"org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)",
"javax.servlet.http.HttpServlet.service(HttpServlet.java:626)",
"org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)",
"javax.servlet.http.HttpServlet.service(HttpServlet.java:733)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)",
"org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)",
"org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93)",
"org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)",
"org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)",
"org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)",
"org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)",
"org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)",
"org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)",
"org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)",
"org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)",
"org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)",
"org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)",
"org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)",
"org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)",
"org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)",
"org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)",
"org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)",
"org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)",
"java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)",
"java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)",
"org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)",
"java.base/java.lang.Thread.run(Thread.java:834)"
]
It seems Dozer is trying to find some xml conffig file taking into account this:
"oracle.xml.jaxp.JXDocumentBuilderFactory.setAttribute(JXDocumentBuilderFactory.java:289)"
It seems it is searching for a xml validation schema (take a look at var1 on the picture below):
When I start the application I just saw the following in the IntelliJ console:
Trying to find Dozer configuration file: dozer.properties
2020-12-23 11:46:09.855 WARN 17488 --- [ restartedMain] org.dozer.config.GlobalSettings: Dozer configuration file not found: dozer.properties. Using defaults for all Dozer global properties.
Probably I have to search for dozer.properties and find out how to make it look for Java configurations?
Can someone help me, please? I searched for some solution in internet but I still haven't found a suitable one. I am new to Dozer, I have used Mapstruct before?
You can try my beanknife to generate the dto file automatically. It will has a read method to convert the entity to dto. Although no converter from dto to entity, I think you don't need it in most situation.
#ViewOf(value = Client.class, genName = "ClientDto", includePattern = ".*")
class ClientDtoConfiguration {}
Then it will generate a dto class named "ClientDto" with all the properties of Client.
Client client = ...
ClientDto clientDto = ClientDto.read(client);
List<Client> clients = ...
List<ClientDto> clientDtos = ClientDto.read(clients);
Then serialize the dtos instead of entities.

How to make AuditorAware work with Spring Data Mongo Reactive

Spring Security 5 provides a ReactiveSecurityContextHolder to fetch the SecurityContext from a Reactive context, but when I want to implement AuditorAware and get audition work automatically, but it does not work. Currently I can not find a Reactive variant for AuditorAware.
#Bean
public AuditorAware<Username> auditor() {
return () -> ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.log()
.filter(a -> a != null && a.isAuthenticated())
.map(Authentication::getPrincipal)
.cast(UserDetails.class)
.map(auth -> new Username(auth.getName()))
.switchIfEmpty(Mono.empty())
.blockOptional();
}
I have added #EnableMongoAuduting on my boot Application class.
On the Mongo document class. I added audition related annotations.
#CreatedDate
private LocalDateTime createdDate;
#CreatedBy
private Username author;
When I added a post, the createdDate is filled, but author is null.
{"id":"5a49ccdb9222971f40a4ada1","title":"my first post","content":"content of my first post","createdDate":"2018-01-01T13:53:31.234","author":null}
The complete codes is here, based on Spring Boot 2.0.0.M7.
Update: Spring Boot 2.4.0-M2/Spring Data Common 2.4.0-M2/Spring Data Mongo 3.1.0-M2 includes a ReactiveAuditorAware, Check this new sample, Note: use #EnableReactiveMongoAuditing to activiate it.
I am posting another solution which counts with input id to support update operations:
#Component
#RequiredArgsConstructor
public class AuditCallback implements ReactiveBeforeConvertCallback<AuditableEntity> {
private final ReactiveMongoTemplate mongoTemplate;
private Mono<?> exists(Object id, Class<?> entityClass) {
if (id == null) {
return Mono.empty();
}
return mongoTemplate.findById(id, entityClass);
}
#Override
public Publisher<AuditableEntity> onBeforeConvert(AuditableEntity entity, String collection) {
var securityContext = ReactiveSecurityContextHolder.getContext();
return securityContext
.zipWith(exists(entity.getId(), entity.getClass()))
.map(tuple2 -> {
var auditableEntity = (AuditableEntity) tuple2.getT2();
auditableEntity.setLastModifiedBy(tuple2.getT1().getAuthentication().getName());
auditableEntity.setLastModifiedDate(Instant.now());
return auditableEntity;
})
.switchIfEmpty(Mono.zip(securityContext, Mono.just(entity))
.map(tuple2 -> {
var auditableEntity = (AuditableEntity) tuple2.getT2();
String principal = tuple2.getT1().getAuthentication().getName();
Instant now = Instant.now();
auditableEntity.setLastModifiedBy(principal);
auditableEntity.setCreatedBy(principal);
auditableEntity.setLastModifiedDate(now);
auditableEntity.setCreatedDate(now);
return auditableEntity;
}));
}
}
Deprecated: see the update solution in the original post
Before the official reactive AuditAware is provided, there is an alternative to implement these via Spring Data Mongo specific ReactiveBeforeConvertCallback.
Do not use #EnableMongoAuditing
Implement your own ReactiveBeforeConvertCallback, here I use a PersistentEntity interface for those entities that need to be audited.
public class PersistentEntityCallback implements ReactiveBeforeConvertCallback<PersistentEntity> {
#Override
public Publisher<PersistentEntity> onBeforeConvert(PersistentEntity entity, String collection) {
var user = ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.filter(it -> it != null && it.isAuthenticated())
.map(Authentication::getPrincipal)
.cast(UserDetails.class)
.map(userDetails -> new Username(userDetails.getUsername()))
.switchIfEmpty(Mono.empty());
var currentTime = LocalDateTime.now();
if (entity.getId() == null) {
entity.setCreatedDate(currentTime);
}
entity.setLastModifiedDate(currentTime);
return user
.map(u -> {
if (entity.getId() == null) {
entity.setCreatedBy(u);
}
entity.setLastModifiedBy(u);
return entity;
}
)
.defaultIfEmpty(entity);
}
}
Check the complete codes here.
To have createdBy attribute filled, you need to link your auditorAware bean with the annotation #EnableMongoAuditing
In your MongoConfig class, define your bean :
#Bean(name = "auditorAware")
public AuditorAware<String> auditor() {
....
}
and use it in the annotation :
#Configuration
#EnableMongoAuditing(auditorAwareRef="auditorAware")
class MongoConfig {
....
}

Manually call Hibernate URL validator in spring-boot

I am using spring-boot 1.4.0 and hibernate-validator 5.2.0. I have a model which contains custom validator inside the custom validator i want to check whether the property value is a valid URL for that i need to call URLValidator in hibernate but no luck.Could anyone please guide me to resolve this issue
CustomValidator.java
#Component
public class BookValidator extends GenericValidator<Book, ConstraintValidatorContext> implements ConstraintValidator<ValidBooks, List<Book>> {
public BookValidator() {
addValidators();
}
private void addValidators() {
getValidators().add((book, context) -> {
boolean isValid = book.getUrl(); //here i want to check against Hibernate URL validator
if (!isValid) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("Book URL should be valid!")
.addConstraintViolation();
}
return isValid;
});
}
#Override
public void initialize(ValidBooks constraintAnnotation) {
}
}
How can i check whether the URL is a valid one
boolean isValid = book.getUrl(); using hibernate URLValidator?
This works:
AnnotationDescriptor<URL> descriptor = new AnnotationDescriptor<URL>( URL.class );
URL url = AnnotationFactory.create(descriptor);
URLValidator urlValidator = new URLValidator();
urlValidator.initialize(url);
boolean isValid = urlValidator.isValid(book.getUrl(), context);

#MessageMapping with placeholders

I am working with Spring-websocket and I have the following problem:
I am trying to put a placeholder inside a #MessageMapping annotation in order to get the url from properties. It works with #RequestMapping but not with #MessageMapping.
If I use this placeholder, the URL is null. Any idea or suggestion?
Example:
#RequestMapping(value= "${myProperty}")
#MessageMapping("${myProperty}")
Rossen Stoyanchev added placeholder support for #MessageMapping and #SubscribeMapping methods.
See Jira issue: https://jira.spring.io/browse/SPR-13271
Spring allows you to use property placeholders in #RequestMapping, but not in #MessageMapping. This is 'cause the MessageHandler. So, we need to override the default MessageHandler to do this.
WebSocketAnnotationMethodMessageHandler does not support placeholders and you need add this support yourself.
For simplicity I just created another WebSocketAnnotationMethodMessageHandler class in my project at the same package of the original, org.springframework.web.socket.messaging, and override getMappingForMethod method from SimpAnnotationMethodMessageHandler with same content, changing only how SimpMessageMappingInfo is contructed using this with this methods (private in WebSocketAnnotationMethodMessageHandler):
private SimpMessageMappingInfo createMessageMappingCondition(final MessageMapping annotation) {
return new SimpMessageMappingInfo(SimpMessageTypeMessageCondition.MESSAGE, new DestinationPatternsMessageCondition(
this.resolveAnnotationValues(annotation.value()), this.getPathMatcher()));
}
private SimpMessageMappingInfo createSubscribeCondition(final SubscribeMapping annotation) {
final SimpMessageTypeMessageCondition messageTypeMessageCondition = SimpMessageTypeMessageCondition.SUBSCRIBE;
return new SimpMessageMappingInfo(messageTypeMessageCondition, new DestinationPatternsMessageCondition(
this.resolveAnnotationValues(annotation.value()), this.getPathMatcher()));
}
These methods now will resolve value considering properties (calling resolveAnnotationValues method), so we need use something like this:
private String[] resolveAnnotationValues(final String[] destinationNames) {
final int length = destinationNames.length;
final String[] result = new String[length];
for (int i = 0; i < length; i++) {
result[i] = this.resolveAnnotationValue(destinationNames[i]);
}
return result;
}
private String resolveAnnotationValue(final String name) {
if (!(this.getApplicationContext() instanceof ConfigurableApplicationContext)) {
return name;
}
final ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) this.getApplicationContext();
final ConfigurableBeanFactory configurableBeanFactory = applicationContext.getBeanFactory();
final String placeholdersResolved = configurableBeanFactory.resolveEmbeddedValue(name);
final BeanExpressionResolver exprResolver = configurableBeanFactory.getBeanExpressionResolver();
if (exprResolver == null) {
return name;
}
final Object result = exprResolver.evaluate(placeholdersResolved, new BeanExpressionContext(configurableBeanFactory, null));
return result != null ? result.toString() : name;
}
You still need to define a PropertySourcesPlaceholderConfigurer bean in your configuration.
If you are using XML based configuration, include something like this:
<context:property-placeholder location="classpath:/META-INF/spring/url-mapping-config.properties" />
If you are using Java based configuration, you can try in this way:
#Configuration
#PropertySources(value = #PropertySource("classpath:/META-INF/spring/url-mapping-config.properties"))
public class URLMappingConfig {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Obs.: in this case, url-mapping-config.properties file are in a gradle/maven project in src\main\resources\META-INF\spring folder and content look like this:
myPropertyWS=urlvaluews
This is my sample controller:
#Controller
public class WebSocketController {
#SendTo("/topic/test")
#MessageMapping("${myPropertyWS}")
public String test() throws Exception {
Thread.sleep(4000); // simulated delay
return "OK";
}
}
With default MessageHandler startup log will print something like this:
INFO: Mapped "{[/${myPropertyWS}],messageType=[MESSAGE]}" onto public java.lang.String com.brunocesar.controller.WebSocketController.test() throws java.lang.Exception
And with our MessageHandler now print this:
INFO: Mapped "{[/urlvaluews],messageType=[MESSAGE]}" onto public java.lang.String com.brunocesar.controller.WebSocketController.test() throws java.lang.Exception
See in this gist the full WebSocketAnnotationMethodMessageHandler implementation.
EDIT: this solution resolves the problem for versions before 4.2 GA. For more information, see this jira.
Update :
Now I understood what you mean, but I think that is not possible(yet).
Documentation does not mention anything related to Path mapping URIs.
Old answer
Use
#MessageMapping("/handler/{myProperty}")
instead of
#MessageMapping("/handler/${myProperty}")
And use it like this:
#MessageMapping("/myHandler/{username}")
public void handleTextMessage(#DestinationVariable String username,Message message) {
//do something
}
#MessageMapping("/chat/{roomId}")
public Message handleMessages(#DestinationVariable("roomId") String roomId, #Payload Message message, Traveler traveler) throws Exception {
System.out.println("Message received for room: " + roomId);
System.out.println("User: " + traveler.toString());
// store message in database
message.setAuthor(traveler);
message.setChatRoomId(Integer.parseInt(roomId));
int id = MessageRepository.getInstance().save(message);
message.setId(id);
return message;
}

Looking for a solution to extend Spring MVC with another Component/Annotation

Suppose I have a Website that is used in normal mode (browser) and in some other mode, like a MobileView mode (inside a mobile app). For each Controller I create, there might be correspondent controller for MobileView, processing the same url.
The easiest solution is to create ifs in all the Controllers that have MobileView logic. Another solution would be to use a correspondent url for MobileView (similar to the normal url) and two separate Controllers (possible where one extends from another; or use some other way to recycle common code)
But, a more elegant solution would be to have some extra annotations, like #SupportsMobileView (to mark a controller, and tell the app that this will have a correspondent MobileView Controller) and #MobileViewController (to mark a second controller, and tell the app that this controller needs to run immediately after the initial controller marked with #SupportsMobileView). The link between a normal controller and a MobileView controller would be through the url they process (defined with #RequestMapping).
Is it possible to extend Spring MVC (A)? Where to inject new annotation scanners (B) and annotation handlers / component handlers (C)? How should the MobileView controller be executed (D) (right now I am thinking that it could be executed through AOP, where the new handler of my new controller type programatically creates a Join-Point on the corresponding normal controller)
Note that I did not mention how this MobileView mode is triggered and detected. Let's just say that there a Session boolean variable (flag) for that.
Critics on any points (A), (B), (C) or (D) are welcomed, as well as technical hints and alternative solution to any point or the whole solution.
HandlerInterceptor can be used to intercept the RequestMapping handling. This is a simple example how to configure and implement one.
You can check for your session variable and will have a bunch of methods that will allow you to do custom processing or just exchange the view from the normal controller handling with your mobile view.
Ok, warnings:
this is only a proof of concept of what I understood must be done so:
+#MobileViewEnable and #MobileView annotated (and related) methods need to stay in the same controller
+there's no check for the httpAction used
+the two methods must have the same signature
+mobileView annotation value and requestMapping annotation value must be equals and uniques
+the logic inside callYourLogic(..) defines which method is going to be called, at the moment there's a very simple logic that check if exist the parameter ("mobile") in the request, just to test
+this code is not intended to be used as is (at all)
+don't know if it works at all outside my pc (joke :D, ehm..)
SO:
Annotations:
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileView {
String value() default "";
}
#Retention(RetentionPolicy.RUNTIME)
public #interface MobileViewEnable {
}
ExampleController:
#Controller
public class MainController extends BaseController {
private final static Logger logger = LoggerFactory.getLogger(MainController.class);
private final static String PROVA_ROUTE = "prova";
#MobileViewEnable
#RequestMapping(PROVA_ROUTE)
public String prova() {
logger.debug("inside prova!!!");
return "provaview";
}
#MobileView(PROVA_ROUTE)
public String prova2() {
logger.debug("inside prova2!!!");
return "prova2view";
}
}
Aspect definition:
<bean id="viewAspect" class="xxx.yyy.ViewAspect" />
<aop:config>
<aop:pointcut expression="#annotation(xxx.yyy.MobileViewEnable)" id="viewAspectPointcut" />
<aop:aspect ref="viewAspect" order="1">
<aop:around method="around" pointcut-ref="viewAspectPointcut" arg-names="viewAspectPointcut"/>
</aop:aspect>
</aop:config>
Aspect implementation:
public class ViewAspect implements BeforeAdvice, ApplicationContextAware {
private final static Logger logger = LoggerFactory.getLogger(ViewAspect.class);
private ApplicationContext applicationContext;
public Object around(ProceedingJoinPoint joinPoint) {
Method mobileViewAnnotatedMethod = null;
HttpServletRequest request = getCurrentHttpRequest();
String controllerName = getSimpleClassNameWithFirstLetterLowercase(joinPoint);
Object[] interceptedMethodArgs = getInterceptedMethodArgs(joinPoint);
String methodName = getCurrentMethodName(joinPoint);
Method[] methods = getAllControllerMethods(joinPoint);
Method interceptedMethod = getInterceptedMethod(methods, methodName);
String interceptedMethodRoute = getRouteFromInterceptedMethod(interceptedMethod);
if (callYourLogic(request)) {
mobileViewAnnotatedMethod = getMobileViewAnnotatedMethodWithRouteName(methods, interceptedMethodRoute);
if (mobileViewAnnotatedMethod != null)
return invokeMethod(mobileViewAnnotatedMethod, interceptedMethodArgs, controllerName);
}
return continueInterceptedMethodExecution(joinPoint, interceptedMethodArgs);
}
private Object continueInterceptedMethodExecution(ProceedingJoinPoint joinPoint, Object[] interceptedMethodArgs) {
try {
return joinPoint.proceed(interceptedMethodArgs);
} catch (Throwable e) {
logger.error("unable to proceed with intercepted method call: " + e);
}
return null;
}
private Object[] getInterceptedMethodArgs(JoinPoint joinPoint) {
return joinPoint.getArgs();
}
private boolean callYourLogic(HttpServletRequest request) {
// INSERT HERE YOUR CUSTOM LOGIC (e.g.: is the server accessed from a mobile device?)
// THIS IS A STUPID LOGIC USED ONLY FOR EXAMPLE
return request.getParameter("mobile")!= null;
}
private HttpServletRequest getCurrentHttpRequest() {
return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
}
private String invokeMethod(Method method, Object[] methodArgs, String className) {
if (method != null) {
try {
Object classInstance = getInstanceOfClass(method, className);
return (String) method.invoke(classInstance, methodArgs);
} catch (Exception e) {
logger.error("unable to invoke method" + method + " - " + e);
}
}
return null;
}
private Object getInstanceOfClass(Method method, String className) {
return applicationContext.getBean(className);
}
private Method getMobileViewAnnotatedMethodWithRouteName(Method[] methods, String routeName) {
for (Method m : methods) {
MobileView mobileViewAnnotation = m.getAnnotation(MobileView.class);
if (mobileViewAnnotation != null && mobileViewAnnotation.value().equals(routeName))
return m;
}
return null;
}
private String getRouteFromInterceptedMethod(Method method) {
RequestMapping requestMappingAnnotation = method.getAnnotation(RequestMapping.class);
if (requestMappingAnnotation != null)
return requestMappingAnnotation.value()[0];
return null;
}
private String getCurrentMethodName(JoinPoint joinPoint) {
return joinPoint.getSignature().getName();
}
private Method[] getAllControllerMethods(JoinPoint joinPoint) {
return joinPoint.getThis().getClass().getSuperclass().getMethods();
}
private String getSimpleClassNameWithFirstLetterLowercase(JoinPoint joinPoint) {
String simpleClassName = joinPoint.getThis().getClass().getSuperclass().getSimpleName();
return setFirstLetterLowercase(simpleClassName);
}
private String setFirstLetterLowercase(String simpleClassName) {
String firstLetterOfTheString = simpleClassName.substring(0, 1).toLowerCase();
String restOfTheString = simpleClassName.substring(1);
return firstLetterOfTheString + restOfTheString;
}
private Method getInterceptedMethod(Method[] methods, String lookingForMethodName) {
for (Method m : methods)
if (m.getName().equals(lookingForMethodName))
return m;
return null;
}
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

Resources