pointcut expression to parse #ModelAttribute as method parameter - java

I have a #ModelAttribute(key) defined in the #controllerAdvice and i am uisng the same model attribute in multiple controller methods as method parameter because the (key) will be available throughout all the controllers.
I am adding the attribute (key) in controller class like this.
#RequestMapping(value = "/", method = RequestMethod.GET)
public String list(final Model model,#ModelAttribute("key") final boolean key) {
...
...
}
I want to intercept all the controller methods which has #ModelAttribute("key") as the method parameter.
My aspect file looks like this.
#Component
#Aspect
#EnableAspectJAutoProxy
public class myAspectclass {
#Pointcut("execution(public * *(.., #org.springframework.web.bind.annotation.ModelAttribute(boolean key)))")
public void methodWithAnnotatedParameter() {}
#Around("methodWithAnnotatedParameter()")
public String blahMethod(ProceedingJoinPoint PJP){
blah blah
....
}
However my server startup fails saying
[tomcat:launch] Caused by: java.lang.IllegalArgumentException: Pointcut is not well-formed: expecting 'name pattern' at character position 97
[tomcat:launch] execution(public * *(.., #org.springframework.web.bind.annotation.ModelAttribute(boolean key), ..))
I am unable to understand the error in this case... am i doing something wrong syntactically ?
Note: My ModelAttribute(key) does not have any specific position in the parameter list
Referenced this answer:

Here is s stand-alone AspectJ example (not a Spring application, but aspect syntax should be the same).
Driver application:
As you can see, there are several methods with and without #ModelAttribute parameter annotations, one of them even with two annotated parameters (whether it makes sense or not, but it is just an example).
package de.scrum_master.app;
import java.util.Collection;
import java.util.Map;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
public class Application {
public String list(Model model, #ModelAttribute("key") boolean key) {
return "x";
}
public String foo(Model model, #ModelAttribute("key") boolean key, #ModelAttribute(name = "key") String text) {
return "x";
}
public String bar(#ModelAttribute(name = "key") int number) {
return "x";
}
public String zot(Model model, #ModelAttribute("XXX") boolean key) {
return "x";
}
public String baz(Model model, boolean key, String text) {
return "x";
}
public String bla(#ModelAttribute("XXX") int number) {
return "x";
}
public static void main(String[] args) {
Model model = new Model() {
#Override public Model mergeAttributes(Map<String, ?> arg0) { return null; }
#Override public boolean containsAttribute(String arg0) { return false; }
#Override public Map<String, Object> asMap() { return null; }
#Override public Model addAttribute(String arg0, Object arg1) { return null; }
#Override public Model addAttribute(Object arg0) { return null; }
#Override public Model addAllAttributes(Map<String, ?> arg0) { return null; }
#Override public Model addAllAttributes(Collection<?> arg0) { return null; }
};
Application application = new Application();
application.list(model, true);
application.foo(model, true, "hey");
application.bar(11);
application.zot(model, true);
application.baz(model, true, "hey");
application.bla(11);
}
}
Aspect:
The aspect matches all method executions having at least one parameter with a #ModelAttribute annotation. The pointcut would be enough if you only want to finde those methods, regardless of annotation parameter values. But as you said, you only want to match the ones which have value = "key", we need to use reflection in order to look into the annotations themselves and filter out the unwanted ones.
Another complication that according to #ModelAttribute JavaDoc, parameters name and value are aliases for each other, i.e. we also need to check both parameters' values in order to get it completely right.
package de.scrum_master.aspect;
import java.lang.annotation.Annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ModelAttribute;
#Aspect
#Component
public class ModelAttributeInterceptor {
#Pointcut("execution(public * *(.., #org.springframework.web.bind.annotation.ModelAttribute (*), ..))")
public void methodWithAnnotatedParameter() {}
#Around("methodWithAnnotatedParameter()")
public String blahMethod(ProceedingJoinPoint thisJoinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature();
Annotation[][] annotationMatrix = methodSignature.getMethod().getParameterAnnotations();
boolean foundModelAttribute = false;
for (Annotation[] annotations : annotationMatrix) {
for (Annotation annotation : annotations) {
if (!(annotation instanceof ModelAttribute))
continue;
ModelAttribute modelAttribute = (ModelAttribute) annotation;
if ("key".equals(modelAttribute.value()) || "key".equals(modelAttribute.name())) {
if (!foundModelAttribute) {
System.out.println(thisJoinPoint);
foundModelAttribute = true;
}
System.out.println(" " + modelAttribute);
}
}
}
return (String) thisJoinPoint.proceed();
}
}
Console log:
execution(String de.scrum_master.app.Application.list(Model, boolean))
#org.springframework.web.bind.annotation.ModelAttribute(name=, value=key, binding=true)
execution(String de.scrum_master.app.Application.foo(Model, boolean, String))
#org.springframework.web.bind.annotation.ModelAttribute(name=, value=key, binding=true)
#org.springframework.web.bind.annotation.ModelAttribute(name=key, value=, binding=true)
execution(String de.scrum_master.app.Application.bar(int))
#org.springframework.web.bind.annotation.ModelAttribute(name=key, value=, binding=true)

Related

How to set default value on field values in a class used in spring boot request parameter

I am creating a GetEndpoint exposed like below
#GetMapping
public void someMethod(#RequestParam(value = "selectedColor", required = false,
defaultValue = "WHITE") Color seletedColor) {
....
}
I need to convert all method parameters to a class object like below. Please let me know how to set default value defaultValue = "WHITE" at field level in below class
#GetMapping
public void someMethod(RequestParameter request) {
....
}
public Class RequestParameter {
// How to set default value if parameter is null
private Color seletedColor;
}
I would suggest using a getter to have this logic. For example:
public class RequestParameter {
...
public Color getSelectedColor() {
return Optional.ofNullabe(selectedColor).orElse(Color.WHITE);
}
}
Once you decide to bind params to a custom request parameter class, you can no longer take advantage of the built-in parameter annotations that make it so easy to set defaults.
If you still want to set default values per-endpoint for one of your request class's field-level parameters, it's actually not too hard.
Make your own custom HandlerMethodArgumentResolver and pair it with a custom annotation that the resolver can check for.
public class MyRequestParameterMethodArgumentResolver implements HandlerMethodArgumentResolver {
public static final String DEFAULT_COLOR = "ffffff";
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(MyRequestParameter.class);
}
#Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
String colorParam = webRequest.getParameter("color");
// defaultColor may be overridden per-endpoint using this custom parameter annotation
DefaultColor defaultColorAnnotation = parameter.getParameterAnnotation(DefaultColor.class);
String defaultColor = defaultColorAnnotation != null ? defaultColorAnnotation.value() : DEFAULT_COLOR;
Color color = Optional.ofNullable(colorParam).map(Color::decode).orElse(Color.decode(defaultColor));
return new MyRequestParameter(color);
}
}
That resolver of course must be registered
public class MyApplication implements WebMvcConfigurer {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MyRequestParameterMethodArgumentResolver());
}
...
and then just define your custom annotation
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface DefaultColor {
#AliasFor("value")
String color() default "";
#AliasFor("color")
String value() default "";
}
and finally, you will have the freedom to give each endpoint its own default color
#GetMapping
public void someMethod( #DefaultColor("ff0000") MyRequestParameter requestParams) {
....
}
#GetMapping
public void someOtherMethod( #DefaultColor("00ff00") MyRequestParameter requestParams) {
....
}

Generic enum JPA AttributeConverter implementation

Problem I am trying to solve
I am trying to implement enum mapping for Hibernate. So far I have researched available options, and both the #Enumerated(EnumType.ORDINAL) and #Enumerated(EnumType.STRING) seemed inadequate for my needs. The #Enumerated(EnumType.ORDINAL) seems to be very error-prone, as a mere reordering of enum constants can mess the mapping up, and the #Enumerated(EnumType.STRING) does not suffice too, as the database I work with is already full of values to be mapped, and these values are not what I would like my enum constants to be named like (the values are foreign language strings / integers).
Currently, all these values are being mapped to String / Integer properties. At the same time the properties should only allow for a restricted sets of values (imagine meetingStatus property allowing for Strings: PLANNED, CANCELED, and DONE. Or another property allowing for a restricted set of Integer values: 1, 2, 3, 4, 5).
My idea was to replace the implementation with enums to improve the type safety of the code. A good example where the String / Integer implementation could cause errors is String method parameter representing such value - with String, anything goes there. Having an Enum parameter type on the other hand introduces compile time safety.
My best approach so far
The only solution that seemed to fulfill my needs was to implement custom javax.persistence.AttributeConverter with #Converter annotation for every enum. As my model would require quite a few enums, writing custom converter for each of them started to seem like a madness really quickly. So I searched for a generic solution to the problem -> how to write a generic converter for any type of enum. The following answer was of big help here: https://stackoverflow.com/a/23564597/7024402. The code example in the answer provides for somewhat generic implementation, yet for every enum there is still a separate converter class needed. The author of the answer also continues:
"The alternative would be to define a custom annotation, patch the JPA provider to recognize this annotation. That way, you could examine the field type as you build the mapping information and feed the necessary enum type into a purely generic converter."
And that's what I think I would be interested in. I could, unfortunately, not find any more information about that, and I would need a little more guidance to understand what needs to be done and how would it work with this approach.
Current Implementation
public interface PersistableEnum<T> {
T getValue();
}
public enum IntegerEnum implements PersistableEnum<Integer> {
ONE(1),
TWO(2),
THREE(3),
FOUR(4),
FIVE(5),
SIX(6);
private int value;
IntegerEnum(int value) {
this.value = value;
}
#Override
public Integer getValue() {
return value;
}
}
public abstract class PersistableEnumConverter<E extends PersistableEnum<T>, T> implements AttributeConverter<E, T> {
private Class<E> enumType;
public PersistableEnumConverter(Class<E> enumType) {
this.enumType = enumType;
}
#Override
public T convertToDatabaseColumn(E attribute) {
return attribute.getValue();
}
#Override
public E convertToEntityAttribute(T dbData) {
for (E enumConstant : enumType.getEnumConstants()) {
if (enumConstant.getValue().equals(dbData)) {
return enumConstant;
}
}
throw new EnumConversionException(enumType, dbData);
}
}
#Converter
public class IntegerEnumConverter extends PersistableEnumConverter<IntegerEnum, Integer> {
public IntegerEnumConverter() {
super(IntegerEnum.class);
}
}
This is how I was able to achieve the partially generic converter implementation.
GOAL: Getting rid of the need to create new converter class for every enum.
Luckily, you should not patch the hibernate for this.
You can declare an annotation like the following:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.sql.Types;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target({METHOD, FIELD})
#Retention(RUNTIME)
public #interface EnumConverter
{
Class<? extends PersistableEnum<?>> enumClass() default IntegerEnum.class;
int sqlType() default Types.INTEGER;
}
A hibernate user type like the following:
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;
import java.util.Properties;
import org.hibernate.HibernateException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.usertype.DynamicParameterizedType;
import org.hibernate.usertype.UserType;
public class PersistableEnumType implements UserType, DynamicParameterizedType
{
private int sqlType;
private Class<? extends PersistableEnum<?>> clazz;
#Override
public void setParameterValues(Properties parameters)
{
ParameterType reader = (ParameterType) parameters.get(PARAMETER_TYPE);
EnumConverter converter = getEnumConverter(reader);
sqlType = converter.sqlType();
clazz = converter.enumClass();
}
private EnumConverter getEnumConverter(ParameterType reader)
{
for (Annotation annotation : reader.getAnnotationsMethod()){
if (annotation instanceof EnumConverter) {
return (EnumConverter) annotation;
}
}
throw new IllegalStateException("The PersistableEnumType should be used with #EnumConverter annotation.");
}
#Override
public int[] sqlTypes()
{
return new int[] {sqlType};
}
#Override
public Class<?> returnedClass()
{
return clazz;
}
#Override
public boolean equals(Object x, Object y) throws HibernateException
{
return Objects.equals(x, y);
}
#Override
public int hashCode(Object x) throws HibernateException
{
return Objects.hashCode(x);
}
#Override
public Object nullSafeGet(ResultSet rs,
String[] names,
SharedSessionContractImplementor session,
Object owner) throws HibernateException, SQLException
{
Object val = null;
if (sqlType == Types.INTEGER) val = rs.getInt(names[0]);
if (sqlType == Types.VARCHAR) val = rs.getString(names[0]);
if (rs.wasNull()) return null;
for (PersistableEnum<?> pEnum : clazz.getEnumConstants())
{
if (Objects.equals(pEnum.getValue(), val)) return pEnum;
}
throw new IllegalArgumentException("Can not convert " + val + " to enum " + clazz.getName());
}
#Override
public void nullSafeSet(PreparedStatement st,
Object value,
int index,
SharedSessionContractImplementor session) throws HibernateException, SQLException
{
if (value == null) {
st.setNull(index, sqlType);
}
else {
PersistableEnum<?> pEnum = (PersistableEnum<?>) value;
if (sqlType == Types.INTEGER) st.setInt(index, (Integer) pEnum.getValue());
if (sqlType == Types.VARCHAR) st.setString(index, (String) pEnum.getValue());
}
}
#Override
public Object deepCopy(Object value) throws HibernateException
{
return value;
}
#Override
public boolean isMutable()
{
return false;
}
#Override
public Serializable disassemble(Object value) throws HibernateException
{
return Objects.toString(value);
}
#Override
public Object assemble(Serializable cached, Object owner) throws HibernateException
{
return cached;
}
#Override
public Object replace(Object original, Object target, Object owner) throws HibernateException
{
return original;
}
}
And then, you can use it:
import org.hibernate.annotations.Type;
#Entity
#Table(name="TST_DATA")
public class TestData
{
...
#EnumConverter(enumClass = IntegerEnum.class, sqlType = Types.INTEGER)
#Type(type = "com.example.converter.PersistableEnumType")
#Column(name="INT_VAL")
public IntegerEnum getIntValue()
...
#EnumConverter(enumClass = StringEnum.class, sqlType = Types.VARCHAR)
#Type(type = "com.example.converter.PersistableEnumType")
#Column(name="STR_VAL")
public StringEnum getStrValue()
...
}
See also the chapter 5.3.3 Extending Hibernate with UserTypes at the excellent book "Java Persistence with Hibernate" by Bauer, King, Gregory.
Simplifying:
import com.pismo.apirest.mvc.enums.OperationType;
import com.pismo.apirest.mvc.enums.support.PersistableEnum;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
#SuppressWarnings("unused")
public interface EnumsConverters {
#RequiredArgsConstructor
abstract class AbstractPersistableEnumConverter<E extends Enum<E> & PersistableEnum<I>, I> implements AttributeConverter<E, I> {
private final E[] enumConstants;
public AbstractPersistableEnumConverter(#NonNull Class<E> enumType) {
enumConstants = enumType.getEnumConstants();
}
#Override
public I convertToDatabaseColumn(E attribute) {
return Objects.isNull(attribute) ? null : attribute.getId();
}
#Override
public E convertToEntityAttribute(I dbData) {
return fromId(dbData, enumConstants);
}
public E fromId(I idValue) {
return fromId(idValue, enumConstants);
}
public static <E extends Enum<E> & PersistableEnum<I>, I> E fromId(I idValue, E[] enumConstants) {
return Objects.isNull(idValue) ? null : Stream.of(enumConstants)
.filter(e -> e.getId().equals(idValue))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(
String.format("Does not exist %s with ID: %s", enumConstants[0].getClass().getSimpleName(), idValue)));
}
}
#Converter(autoApply = true)
class OperationTypeConverter extends AbstractPersistableEnumConverter<OperationType, Integer> {
public OperationTypeConverter() {
super(OperationType.class);
}
}
}
I tried 1000 times create something same.
Generate converter for each enum on the fly - not problem, but then they will be have same class. Main problem there: org.hibernate.boot.internal.MetadataBuilderImpl#applyAttributeConverter(java.lang.Class<? extends javax.persistence.AttributeConverter>, boolean).
If converter already registered we got exception.
public void addAttributeConverterInfo(AttributeConverterInfo info) {
if ( this.attributeConverterInfoMap == null ) {
this.attributeConverterInfoMap = new HashMap<>();
}
final Object old = this.attributeConverterInfoMap.put( info.getConverterClass(), info );
if ( old != null ) {
throw new AssertionFailure(
String.format(
"AttributeConverter class [%s] registered multiple times",
info.getConverterClass()
)
);
}
}
Perhaps we can change org.hibernate.boot.internal.BootstrapContext Impl, but I'm sure it's create too complex and non-flexible code.

Can I make it mandatory to include at least one of two headers with Spring?

One of my headers is misspelled, and I want to change it while being backwards compatible.
#RequestHeader(value = "Custmer-Key") String customerKey
I want to add a header with the correct spelling Customer-Key, and make at least one of them mandatory. Any ideas?
I'll make a few assumptions here. Each one may or may not be correct in your specific case, but the purpose is to give better context on when such solution is viable and makes sense to use.
You have a need to keep backward compatibility (this one is easy... you wrote it)
You have a pretty large codebase possibly based on microservices and maintained by several developers and you want to avoid large commits spanning across several teams, centralising the fix in a common shared library that all services are meant to use
Your headers are fetched using not just Spring but occasionally also by accessing the request directly
You are working in a production application where you want to change as little code as possible as some of its inner workings are difficult to understand
The solution consists into wiring a custom filter, along with its configuration. The filter will swap the HttpServletRequest instance with a different one that allows to manipulate the headers.
First, create your own filter, as follows:
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE)
public class HeadersFilter implements Filter {
private static final String WRONG_HEADER = "Custmer-Key";
private static final String RIGHT_HEADER = "Customer-Key";
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String newHeaderValue = request.getHeader(RIGHT_HEADER);
String headerValue;
if(newHeaderValue != null) {
headerValue = newHeaderValue;
}
else {
headerValue = request.getHeader(WRONG_HEADER);
}
HeadersRewriteHttpServletRequestWrapper requestWrapper = new HeadersRewriteHttpServletRequestWrapper(request);
requestWrapper.setCustomHeader(WRONG_HEADER, headerValue);
filterChain.doFilter(requestWrapper, response);
}
public static class HeadersRewriteHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> customHeaders;
HeadersRewriteHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
customHeaders = new HashMap<>();
}
void setCustomHeader(String name, String value) {
customHeaders.put(name, value);
}
private String getCustomHeader(String name) {
return customHeaders.get(name);
}
#Override
public String getHeader(String name) { // not needed by spring but useful if someone uses this method directly
String header = super.getHeader(name);
if(header != null) {
return header;
}
return getCustomHeader(name);
}
#Override
public Enumeration<String> getHeaderNames() {
Set<String> names = new HashSet<>(Collections.list(super.getHeaderNames()));
names.addAll(customHeaders.keySet());
return Collections.enumeration(names);
}
#Override
public Enumeration<String> getHeaders(String name) {
List<String> headers = Collections.list(super.getHeaders(name));
String customHeader = getCustomHeader(name);
if(headers.isEmpty() && customHeader != null) {
headers.add(customHeader);
}
return Collections.enumeration(headers);
}
}
}
Second, wire in the Spring configuration to create an instance of this filter and inject it as necessary.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration
public class FilterConfiguration {
#Bean
public HeadersFilter headersFilterBean() {
return new HeadersFilter();
}
}
That's it. Assuming your application doesn't have quirks preventing this from working (in which case good luck with your debugging), this code will take the contents of both Customer-Key and Custmer-Key, giving precedence to Customer-Key and write them in a fake Custmer-Key header. This way you won't have to touch any of the controllers, which should continue to work transparently.
Next Approach is to create a annotation OneOf or something. I have used a simpler approach than using Aspect. Using this approach you can validate request param , Requestbody and RequestHeader
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = OneOfValidator.class)
#Documented
public #interface OneOf {
String message() default "";
String[] value();
}
Create a validator class like below.
public class OneOfValidator implements ConstraintValidator<OneOf, Object> {
private String[] fields;
private String fieldList;
public void initialize(OneOf annotation) {
this.fields = annotation.value();
fieldList = Arrays.toString(fields);
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);
int matches = countNumberOfMatches(wrapper);
if (matches > 1) {
setErrorMessage(context, <your message>);
return false;
} else if (matches == 0) {
setErrorMessage(context, <your message>);
return false;
}
return true;
}
private int countNumberOfMatches(BeanWrapper wrapper) {
int matches = 0;
for (String field : fields) {
Object value = wrapper.getPropertyValue(field);
boolean isPresent = detectOptionalValue(value);
if (value != null && isPresent) {
matches++;
}
}
return matches;
}
private boolean detectOptionalValue(Object value) {
if (value instanceof Optional) {
return ((Optional)value).isPresent();
}
if (value instanceof String) {
return StringUtils.hasText((String)value);
}
return true;
}
private void setErrorMessage(ConstraintValidatorContext context, String template) {
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate(template)
.addNode(fieldList)
.addConstraintViolation();
}
In the controller you can create something like below.
#GetMapping(value = "your path")
public ResponseEntity<HeaderDataDTO> getBuildDetails(#RequestHeader(value = "Custmer-Key") String custmerKey,#RequestHeader(value = "Customer-Key") String customerKey
) {
HeaderDataDTO data = new HeaderDataDTO();
data.setCustomerKey(customerKey);
data.setCustmerKey(custmerKey);
data.validate();
return new ResponseEntity<>(data,
HttpStatus.OK);
}
You can define your DTO as below.
#Valid
#OneOf(value = {"customerKey", "custmerKey"})
public class HeaderDataDTO extends HeaderValidator {
private String customerKey;
private String custmerKey;
//getter and setter
HeaderValidator should be like below. Validate method will validate the object.
import org.springframework.util.CollectionUtils;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import javax.validation.Validator;
public abstract class HeaderValidator {
public boolean validate() {
Validator validator = Validation
.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<HeaderValidator>> violations = validator.validate(this);
if (!CollectionUtils.isEmpty(violations)) {
throw <your exception>
}
return true;
}
You can create a interceptor like below.
#Component
#Primary
public class HeadersInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
HttpInputMessage inputMessage=new ServletServerHttpRequest(request);
HttpHeaders httpHeaders = inputMessage.getHeaders();
//validation code for header goes here.
//return true if validation is successful
return true;
}
}
and add the interceptor to your configuration.
#Configuration
public class InterceptorConfig implements WebMvcConfigurer {
#Autowired
HeadersInterceptor headersInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(headersInterceptor);
}
}`
Now you can customize your validation in any manner.

AspectJ: Intercept return result of method inside another method

I need help to write some Aspectj advice on this particular case:
Suppose we have this class:
package org.group;
public class Person {
public void method1(String id, String number) {
//some code
List<String> list = getList(number);
//some code
}
public List<String> getList(String number) {
return Arrays.asList(number);
}
}
I want to create an Aspectj advice into method1 to get the result of getList. I try this:
#Pointcut("execution(* org.group.Person.getList(..))")
public void methodGetList() {
}
#AfterReturning(pointcut = "methodGetList()", returning = "result")
public void afterMethodGetList(JoinPoint joinPoint, List<String> result) {
System.out.println("I can see the list result: " + result.toString());
}
This advice works on all executions of getList method, but what I want exactly, is to get the result inside the method1 call to get an information with the method1's id , for example like this:
'I can see the list result [4] for the person with id : XXX'
Thank you for your help.
You need to limit your pointcut to the executions within the control flow - cflow() - of the calling method and also bind the calling method's parameter of interest via args().
Application:
package org.group;
import java.util.Arrays;
import java.util.List;
public class Person {
public void method1(String id, String number) {
// some code
List<String> list = getList(number);
// some code
}
public List<String> getList(String number) {
return Arrays.asList(number);
}
public static void main(String[] args) {
// Should not be intercepted
new Person().getList("22");
// Should be intercepted
new Person().method1("John Doe", "11");
}
}
Aspect:
package de.scrum_master.aspect;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
#Aspect
public class MyAspect {
#Pointcut("execution(* org.group.Person.getList(..))")
public void methodGetList() {}
#Pointcut("execution(* org.group.Person.method1(..)) && args(id, *)")
public void methodMethod1(String id) {}
#AfterReturning(
pointcut = "methodGetList() && cflow(methodMethod1(id))",
returning = "result"
)
public void afterMethodGetList(JoinPoint joinPoint, String id, List<String> result) {
System.out.println(
"I can see the list result " + result +
" for the person with id " + id
);
}
}
Console log:
I can see the list result [11] for the person with id John Doe

Read another parameter within jersey's ParamConverter

I've made a ParamConverter which provides an Instant (Date) when given a string formatted as either Instant's native ISO-8601, or as an integer number of milliseconds since the epoch. This is working fine, but I also need to be able to support other date formats (the customers are fussy).
To avoid the classic dd/mm/yyyy vs mm/dd/yyyy ambiguity, I'd like to have the customer specify their preferred format as part of the request*. e.g:
GET http://api.example.com/filter?since=01/02/2000&dateformat=dd/mm/yyyy
passed to a method which looks like:
#GET
String getFilteredList( final #QueryParam( "since" ) Instant since ) {
...
}
(time & timezone parts omitted for clarity)
So I'd like my ParamConverter<Instant> to be able to read the dateformat parameter.
I've been able to use a combination of a filter which sets a ContainerRequestContext property and an AbstractValueFactoryProvider to do something similar, but that needs the parameter to have a custom annotation applied and doesn't let it work with QueryParam/FormParam/etc., making it far less useful.
Is there any way to get other parameters, or the request object itself, from inside a ParamConverter?
[*] In the real world this would be from a selection of pre-approved formats, but for now just assume they're providing the input to a DateTimeFormatter
For clarity, here's the code I have:
public class InstantParameterProvider implements ParamConverterProvider {
private static final ParamConverter<Instant> INSTANT_CONVERTER =
new ParamConverter<Instant>( ) {
#Override public final T fromString( final String value ) {
// This is where I would like to get the other parameter's value
// Is it possible?
}
#Override public final String toString( final T value ) {
return value.toString( );
}
};
#SuppressWarnings( "unchecked" )
#Override public <T> ParamConverter<T> getConverter(
final Class<T> rawType,
final Type genericType,
final Annotation[] annotations
) {
if( rawType == Instant.class ) {
return (ParamConverter<T>) INSTANT_CONVERTER;
}
return null;
}
}
As mentioned here, the key to this is injecting some context object with javax.inject.Provider, which allows us to retrieve the object lazily. Since the ParamConverterProvider is a component managed by Jersey, we should be able to inject other components.
The problem is that the component we need is going to be in a request scope. To get around that, we inject javax.inject.Provider<UriInfo> into the provider. When we actually call get() in the Provider to get the actual instance of UriInfo, it will be be in a request. The same goes for any other component that requires a request scope.
For example
public class InstantParamProvider implements ParamConverterProvider {
#Inject
private javax.inject.Provider<UriInfo> uriInfoProvider;
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (rawType != Instant.class) return null;
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
UriInfo uriInfo = uriInfoProvider.get();
String format = uriInfo.getQueryParameters().getFirst("date-format");
if (format == null) {
throw new WebApplicationException(Response.status(400)
.entity("data-format query parameter required").build());
} else {
try {
// parse and return here
} catch (Exception ex) {
throw new WebApplicationException(
Response.status(400).entity("Bad format " + format).build());
}
}
}
#Override
public String toString(T value) {
return value.toString();
}
};
}
}
UPDATE
Here is a complete example using Jersey Test Framework
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.matchers.JUnitMatchers.*;
public class LocalDateTest extends JerseyTest {
public static class LocalDateParamProvider implements ParamConverterProvider {
#Inject
private javax.inject.Provider<UriInfo> uriInfoProvider;
#Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (rawType != LocalDate.class) {
return null;
}
return new ParamConverter<T>() {
#Override
public T fromString(String value) {
UriInfo uriInfo = uriInfoProvider.get();
String format = uriInfo.getQueryParameters().getFirst("date-format");
if (format == null) {
throw new WebApplicationException(Response.status(400)
.entity("date-format query parameter required").build());
} else {
try {
return (T) LocalDate.parse(value, DateTimeFormatter.ofPattern(format));
// parse and return here
} catch (Exception ex) {
throw new WebApplicationException(
Response.status(400).entity("Bad format " + format).build());
}
}
}
#Override
public String toString(T value) {
return value.toString();
}
};
}
}
#Path("localdate")
public static class LocalDateResource {
#GET
public String get(#QueryParam("since") LocalDate since) {
return since.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"));
}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(LocalDateResource.class)
.register(LocalDateParamProvider.class)
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
#Test
public void should_return_bad_request_with_bad_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.queryParam("date-format", "yyyy/MM/dd")
.request().get();
assertEquals(400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("format yyyy/MM/dd"));
response.close();
}
#Test
public void should_return_bad_request_with_no_date_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.request().get();
assertEquals(400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("query parameter required"));
response.close();
}
#Test
public void should_succeed_with_correct_format() {
Response response = target("localdate")
.queryParam("since", "09/20/2015")
.queryParam("date-format", "MM/dd/yyyy")
.request().get();
assertEquals(200, response.getStatus());
assertThat(response.readEntity(String.class), containsString("09/20/2015"));
response.close();
}
}
Here's the test dependency
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>${jersey2.version}</version>
<scope>test</scope>
</dependency>

Categories