How to get #PathVariable value for custom annotation? - java

I have a controller:
#Authorised(id = "{personId}")
#RequestMapping(value = {"{personId}"}, method = GET)
public void test(#PathVariable PersonId personId) {
System.out.println(personId); //gets personId
}
Annotation:
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Authorised {
String id() default "";
}
Pointcut:
#Pointcut("#annotation(Authorised)")
private void AuthorisedMethod() {}
And the method that has to get {personId} value not string "{personId}":
#Before("AuthorisedMethod()")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getMethod().getName();
Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
Parameter[] parameters = signature.getMethod().getParameters();
Authorised annotations = joinPoint.getTarget().getClass().getMethod(methodName, parameterTypes).getAnnotation(Authorised.class);
String id = annotations.id();
System.out.println(id); // prints: "{personId}"
// do the chekcing
throw new UnauthenticatedUserException();
}
Can it be achieved and how?
UPDATE: But what if method argument parameter number don't match with the pointcut args()? I mean that what if specific method has parameter #PathVariable PersonId personId and few more, but poincut needs to know only PersonId personId?
Like #statut said you have to write args() like that: args(personId,..)

You can modify #Before() annotation to have PersonId value and pass this value to aspect, for example
#Before("AuthorisedMethod() && args(personId)")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint, PersonId personId) throws NoSuchMethodException {}
To test it I had the following Aspect:
#Aspect
#Component
public class SomeAspect {
#Pointcut("#annotation(Authorised)")
private void AuthorisedMethod() {
}
#Before("AuthorisedMethod() && args(personId)")
public void checkIfIsCurrentlyAuthenticated(JoinPoint joinPoint, PersonId personId) throws NoSuchMethodException {
System.out.println("aspect " + personId.getId());
}
}
Configuration class:
#Configuration
#ComponentScan(basePackages = {"test"})
#EnableAspectJAutoProxy(proxyTargetClass = true)
public class Config {
}
Test component:
#Component
public class Test {
#Authorised(id = "{personId}")
public void test(PersonId personId) {
System.out.println("component " + personId.getId()); //gets personId
}
}
And testNG's runner:
#ContextConfiguration(classes = Config.class)
public class TestRunner extends AbstractTestNGSpringContextTests {
#Autowired
test.Test test;
#Test
public void testName() {
test.test(new PersonId("id"));
}
}
When I run it, I get printed "aspect id" from aspect and "component id" from invoked method.

You can also get the value of the PathVariable in RequestMapping URL using HandlerInterceptor if that is possible for you.
Write a Handler class that intercepts this Request.
public class AuthorisedHandler extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!isResourceHandler(handler) && (handler instanceof HandlerMethod)) {
HandlerMethod hm = (HandlerMethod) handler;
Method method = hm.getMethod();
Authorised authAnnotation = method.getAnnotation(Authorised.class);
if (authAnnotation != null) {
String personId = getPersonId(request);
//Do all your validations Here
}
}
return true;
}
#SuppressWarnings("rawtypes")
private String getPersonId(HttpServletRequest request) throws IOException {
String personId = request.getParameter("personId");
if(personId == null || personId.equals("")){
Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
personId = (String) pathVariables.get("personId");
}
return personId;
}
private boolean isResourceHandler(Object handler) {
return handler instanceof ResourceHttpRequestHandler;
}
}
And you must configure this Handler bean in spring config xml or Spring Java Config.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<bean class="com.**.AuthorisedHandler" />
</mvc:interceptor>
</mvc:interceptors>
Now, all the requests will go through this Interceptor. Only which are annotated with #Authorised will go through.

Related

Custom annotation does not work in method

I created annotation to check permissions on the APIs. Annotation worked when placed in class or method. But when I use it for both class and method at the same time, the annotation placed on the method is ignored
Here is an example:
#RestController
#RequestMapping("/user")
#CustomAnnotation(permission="manageUser")
public class UserController {
#CustomAnnotation(permission="updateUser")
#PutMapping("/{id}")
public UserDto getUser(HttpServletRequest request) {
...
}
}
Now #CustomAnnotation(permission="updateUser") in the getUser() method will be ignored and only execute annation in class UserController
My custom code
#Target(value = {ElementType.METHOD, ElementType.TYPE})
#Retention(value = RetentionPolicy.RUNTIME)
#Inherited
#Documented
public #interface CustomAuthorize {
public String permission() default "";
public String attribute() default "";
}
#Aspect
#Component
public class CustomAnnotationAspect {
#Autowired
CustomAuthorizationImpl authBean;
#Pointcut("#annotation(customAuthorize) || #within(customAuthorize)")
public void pointcutForCustomAnnotation(CustomAuthorize customAuthorize) {
// Do nothing.
}
#Around("pointcutForCustomAnnotation(customAuthorize)")
public Object customAspect(ProceedingJoinPoint pjp, CustomAuthorize customAuthorize) throws Throwable {
if (Objects.isNull(customAuthorize)) {
System.out.println("Call from method");
// Check permission
return pjp.proceed();
}
System.out.println("Call from class");
ExpressionParser elParser = new SpelExpressionParser();
if (!customAuthorize.permission().isBlank()) {
// check permission
}
return pjp.proceed();
}
}

implement custom annotation in spring boot environment

I want to create and implement annotation in spring boot environment
Get cookie value through HttpServletRequest to get UserDto from some service
And I want to insert it through the annotation below (#UserInfo), but I don't know how to access it
like below code
#RequestMapping("/test")
public test (#UserInfo UserDto userDto) {
Syste.out.println(userDto.getUserId());
}
Here is an example. I don't have all requirements but I think it will be enough to help you:
#Aspect
#Component
public class AspectConf {
#Autowired
private UserService userService;
#Pointcut("execution(#org.springframework.web.bind.annotation.RequestMapping * *(..))")
public void requestMappingAnnotatedMethod() {}
#Before("requestMappingAnnotatedMethod()")
public void beforeAuthorizeMethods(final JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
//do something with cookies: request.getCookies();
Object userInfoAnnotatedArgument = getUserInfoAnnotatedParameter(joinPoint);
if(userInfoAnnotatedArgument != null) {
((UserDto)userInfoAnnotatedArgument).setName("xsxsxsxsx");
//get `userInfo` from `userService` and update `dto`
((UserDto)userInfoAnnotatedArgument).setXXX(...);
}
}
private Object getUserInfoAnnotatedParameter(final JoinPoint joinPoint) {
final MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
Object[] arguments = joinPoint.getArgs();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Annotation[] annotations = parameters[i].getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == UserInfo.class) {
return arguments[i];
}
}
}
return null;
}
}

How to dynamically disable specific API in spring?

I have a flag DISABLE_FLAG and I want to use it to control multiple specific APIs in different controllers.
#RestController
public final class Controller1 {
#RequestMapping(value = "/foo1", method = RequestMethod.POST)
public String foo1()
}
#RestController
public final class Controller2 {
#RequestMapping(value = "/foo2", method = RequestMethod.POST)
public String foo2()
}
I can use an interceptor to handle all the urls. Is there a easy way to do that like annotation?
You could use AOP to do something like that.
Create your own annotation...
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Maybe { }
and corresponding aspect...
#Aspect
public class MaybeAspect {
#Pointcut("#annotation(com.example.Maybe)")
public void callMeMaybe() {}
#Around("callMeMaybe()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// do your logic here..
if(DISABLE_FOO) {
// do nothing ? throw exception?
// return null;
throw new IllegalStateException();
} else {
// process the request normally
return joinPoint.proceed();
}
}
}
I don't think there is direct way to disable a constructed request mapping but We can disable API in many ways with some condition.
Here is the 2 ways disabling by spring profile or JVM properties.
public class SampleController {
#Autowired
Environment env;
#RequestMapping(value = "/foo", method = RequestMethod.POST)
public String foo(HttpServletResponse response) {
// Using profile
if (env.acceptsProfiles("staging")) {
response.setStatus(404);
return "";
}
// Using JVM options
if("true".equals(System.getProperty("DISABLE_FOO"))) {
response.setStatus(404);
return "";
}
return "";
}
}
If you are thinking futuristic solution using cloud config is the best approach. https://spring.io/guides/gs/centralized-configuration/
Using Conditional components
This allows to build bean with conditions, if the condition failed on startup, the entire component will never be built. Group all your optional request mapping to new controller and add conditional annotation
#Conditional(ConditionalController.class)
public class SampleController {
#Autowired
Environment env;
#RequestMapping(value = "/foo", method = RequestMethod.POST)
public String foo(HttpServletResponse response) {
return "";
}
public static class ConditionalController implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().acceptsProfiles("staging"); // Or whatever condition
}
}
}
You can solve this with annotations by utilizing spring profiles. You define two profiles one for enabled flag and another profile for the disabled flag. Your example would look like this:
#Profile("DISABLED_FLAG")
#RestController
public final class Controller1 {
#RequestMapping(value = "/foo1", method = RequestMethod.POST)
public String foo1()
}
#Profile("ENABLED_FLAG")
#RestController
public final class Controller2 {
#RequestMapping(value = "/foo2", method = RequestMethod.POST)
public String foo2()
}
Here is the link to the spring framework documentation for this feature: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Profile.html
I did it as follows :
#Retention(RUNTIME)
#Target(ElementType.METHOD)
public #interface DisableApiControl {
}
This class is my customization statement. After could use AOP :
for AbstractBaseServiceImpl :
public abstract class AbstractBaseServiceImpl {
private static boolean disableCheck = false;
public void setDisableChecker(boolean checkParameter) {
disableCheck = checkParameter;
}
public boolean getDisableChecker() {
return disableCheck;
}
}
NOTE : The above class has been prepared to provide a dynamic structure.
#Aspect
#Component
public class DisableApiControlAspect extends AbstractBaseServiceImpl {
#Autowired
private HttpServletResponse httpServletResponse;
#Pointcut(" #annotation(disableMe)")
protected void disabledMethods(DisableApiControl disableMe) {
// comment line
}
#Around("disabledMethods(disableMe)")
public Object dontRun(ProceedingJoinPoint joinPoint, DisableApiControl disableMe) throws Throwable {
if (getDisableChecker()) {
httpServletResponse.sendError(HttpStatus.NOT_FOUND.value(), "Not found");
return null;
} else {
return joinPoint.proceed();
}
}
}
checker parameter added global at this point. The rest will be easier when the value is given as true / false when needed.
#GetMapping("/map")
#DisableApiControl
public List<?> stateMachineFindMap() {
return new ArrayList<>;
}

Setting a Jersey response status code from interface without returning Response

I am trying to set a response status of the following Jersey REST endpoint
#Path("/roles")
public interface IRoleService {
#POST
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
Role create(Role role);
}
Since it creates a new resource it would be appropriate if it returned Status code 201 but currently it returns 200.
The only way I found how to set the status code is to have the method return a javax.ws.rs.core.Response and set it there, but I really do not want all of my interfaces to return a generic Response instead of the actual response object (in this case Role).
One way would be to create a custom annotation and using a response filter to set the status. For example
Annotation
#NameBinding
#Target({METHOD, TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface Status {
int DEFAULT_CODE = 0;
int code() default DEFAULT_CODE;
}
ContainerResponseFilter
#Status
#Provider
public class StatusFilter implements ContainerResponseFilter {
#Context
private ResourceInfo info;
#Override
public void filter(ContainerRequestContext req, ContainerResponseContext res) throws IOException {
Status status = getInterfaceAnnotation(info.getResourceMethod());
if (status != null) {
int code = status.code();
if (code != Status.DEFAULT_CODE && res.getStatus() == 200) {
res.setStatus(code);
}
}
}
private static Status getInterfaceAnnotation(Method resourceMethod) {
String methodName = resourceMethod.getName();
Class<?>[] paramTypes = resourceMethod.getParameterTypes();
Class<?> iface = resourceMethod.getDeclaringClass().getInterfaces()[0];
Method ifaceMethod;
try {
ifaceMethod = iface.getDeclaredMethod(methodName, paramTypes);
} catch (NoSuchMethodException e) {
return null;
}
return ifaceMethod.getAnnotation(Status.class);
}
}
In the filter, we get the method with ResourceInfo and do some reflection to get the #Status annotation. The from there, can grab the status code and set it on the response.
Since it is a Name Binding filter, it will only be called for methods that are annotated with it. See more here.
Then to use it, just add the annotation to the method.
public interface ITestResource {
#GET
#Status(code=201)
String get();
}
The same could be done for headers if you need to add some custom headers.
Removed ElementType.Type.
#NameBinding
#Target({ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
public #interface Status {
int DEFAULT_CODE = 0;
int code() default DEFAULT_CODE;
}
statusFilter class:
#Provider
public class StatusFilter implements ContainerResponseFilter {
private static Logger logger = LoggerFactory.getLogger(StatusFilter.class);
#Context
private ResourceInfo resourceInfo;
#Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
Status status = resourceInfo.getResourceMethod().getAnnotation(Status.class);
if(status!=null){
int code = status.code();
if(code != Status.DEFAULT_CODE && responseContext.getStatus() == 200) {
responseContext.setStatus(code);
}
}
}
}
Then use it in the resource interface method declaration
#POST
#Status(code = 201)
#Produces(MediaType.APPLICATION_JSON)
#Consumes(MediaType.APPLICATION_JSON)
GetUserResponse createUser(UserRequest userRequest);

#Valid with spring annotations

I've enabled spring mvc annotation driven for my project. The idea is to use #Valid annotation with spring annotations to avoid the line in the controller like:
validator.validate(form, errors)
I've noticed that this stuff does not work with spring annotations from package:
org.springmodules.validation.bean.conf.loader.annotation.handle
After investigation I've found that I can use annotations from javax or org.hibernate.validator.constraints as alternative way.
But unfortunatly, I have some special cases when I cannot achieve this:
#MinSize(applyIf = "name NOT EQUALS 'default'", value = 1)
Will be good to know in which way spring annotation can be used with #Valid or any other alternative way to avoid refactoring related to applyIf attributes(Move conditions to java code).
Here is an example how to create a custom Validator.
At first create your own Annotation:
#Target({ ElementType.METHOD, ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#Constraint(validatedBy = StringValidator.class)
public #interface ValidString {
String message() default "Invalid data";
int min() default -1;
int max() default -1;
String regex() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Then you will need a custom validator:
public class StringValidator implements ConstraintValidator<ValidString, String> {
private int _min;
private int _max;
private String _regex;
private boolean _decode;
public void initialize(ValidString constraintAnnotation) {
_min = constraintAnnotation.min();
_max = constraintAnnotation.max();
_regex = constraintAnnotation.regex();
_decode = constraintAnnotation.decode();
}
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
String test = value.trim();
if (_min >= 0) {
if (test.length() < _min) {
return false;
}
}
if (_max > 0) {
if (test.length() > _max) {
return false;
}
}
if (_regex != null && !_regex.isEmpty()) {
if (!test.matches(_regex)) {
return false;
}
}
return true;
}
}
Finally you can use it in your Beans and Controller:
public class UserForm {
#ValidString(min=4, max=20, regex="^[a-z0-9]+")
private String name;
//...
}
// Method from Controller
#RequestMapping(method = RequestMethod.POST)
public String saveUser(#Valid UserForm form, BindingResult brResult) {
if (brResult.hasErrors()) {
//TODO:
}
return "somepage";
}
Something like this might help you out
public class UserValidator implements Validator {
#Override
public boolean supports(Class clazz) {
return User.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
User user = (User) target;
if(user.getName() == null) {
errors.rejectValue("name", "your_error_code");
}
// do "complex" validation here
}
}
Then in your controller you would have :
#RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, #ModelAttribute("user") User user, BindingResult result){
UserValidator userValidator = new UserValidator();
userValidator.validate(user, result);
if (result.hasErrors()){
// do something
}
else {
// do something else
}
}
If there are validation errors, result.hasErrors() will be true.
Note : You can also set the validator in a #InitBinder method of the controller, with "binder.setValidator(...)" . Or you could instantiate it in the default constructor of the controller. Or have a #Component/#Service UserValidator that you inject (#Autowired) in your controller : very useful, because most validators are singletons + unit test mocking becomes easier + your validator could call other Spring components.

Categories

Resources