How to implement #RequestMapping custom properties - java

As an example, take subdomain mapping.
This article: Managing multiple Domain and Sub Domain on Google App Engine for Same Application
recommends to resolve subdomain on Filter and assign variable to ServletRequest headers.
Then the mapping will look like this:
#RequestMapping(value = "/path", headers="subdomain=www")
public String subsiteIndexPage(Model model,HttpServletRequest request) { ... }
If we'd like to create custom #RequestMapping property, such as subdomain, eg. to create mapping like this:
#RequestMapping(value = "/some/action", subdomain = "www")
public String handlerFunction(){ ... }
we should override #RequestMapping #interface definition and override RequestMappingHandlerMapping protected methods, with our own implementation
(as stated on JIRA: "Allow custom request mapping conditions SPR-7812").
Is it right? Can anybody provide a hint, how to achieve this functionality?
Idea 1:
As suggested on original jira thread, is to create own implementation of RequestCondition
There is an project which uses this solution available on github: https://github.com/rstoyanchev/spring-mvc-31-demo/
And related SO question: Adding custom RequestCondition's in Spring mvc 3.1
Maybe mapping like #Subdomain("www") for both Type and Method, is possible solution?
Link to same question on forum.springsource.com

I've created solution based on referenced spring-mvc-31-demo
This solution can be used to map only single RequestCondition as of now. I've created two Issues to notify, this should be changed:
https://github.com/rstoyanchev/spring-mvc-31-demo/issues/5
https://jira.springsource.org/browse/SPR-9350
This solution uses custom #RequestCondition feature of Spring 3.1.1.RELEASE platform
USAGE
Example 1:
#Controller
#SubdomainMapping(value = "subdomain", tld = ".mydomain.com")
class MyController1 {
// Code here will be executed only on address match:
// subdomain.mydomain.com
}
Example 2:
#Controller
class MyController2 {
#RequestMapping("/index.html")
#SubdomainMapping("www")
public function index_www(Map<Object, String> map){
// on www.domain.com
// where ".domain.com" is defined in SubdomainMapping.java
}
#RequestMapping("/index.html")
#SubdomainMapping("custom")
public function index_custom(Map<Object, String> map){
// on custom.domain.com
// where ".domain.com" is defined in SubdomainMapping.java
}
}
We need three files
SubdomainMapping.java
SubdomainRequestCondition.java
SubdomainRequestMappingHandlerMapping.java
SubdomainMapping.java
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;
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
public #interface SubdomainMapping {
/**
* This param defines single or multiple subdomain
* Where the Method/Type is valid to be called
*/
String[] value() default {};
/**
* This param defines site domain and tld
* It's important to put the leading dot
* Not an array, so cannot be used for mapping multiple domains/tld
*/
String tld() default ".custom.tld";
}
SubdomainRequestCondition.java
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
public class SubdomainRequestCondition implements
RequestCondition<SubdomainRequestCondition> {
private final Set<String> subdomains;
private final String tld;
public SubdomainRequestCondition(String tld, String... subdomains) {
this(tld, Arrays.asList(subdomains));
}
public SubdomainRequestCondition(String tld, Collection<String> subdomains) {
this.subdomains = Collections.unmodifiableSet(new HashSet<String>(
subdomains));
this.tld = tld;
}
#Override
public SubdomainRequestCondition combine(SubdomainRequestCondition other) {
Set<String> allRoles = new LinkedHashSet<String>(this.subdomains);
allRoles.addAll(other.subdomains);
return new SubdomainRequestCondition(tld, allRoles);
}
#Override
public SubdomainRequestCondition getMatchingCondition(
HttpServletRequest request) {
try {
URL uri = new URL(request.getRequestURL().toString());
String[] parts = uri.getHost().split(this.tld);
if (parts.length == 1) {
for (String s : this.subdomains) {
if (s.equalsIgnoreCase(parts[0])) {
return this;
}
}
}
} catch (Exception e) {
e.printStackTrace(System.err);
}
return null;
}
#Override
public int compareTo(SubdomainRequestCondition other,
HttpServletRequest request) {
return org.apache.commons.collections.CollectionUtils.removeAll(other.subdomains, this.subdomains).size();
}
}
SubdomainRequestMappingHandlerMapping.java
import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class CustomRequestMappingHandlerMapping extends
RequestMappingHandlerMapping {
#Override
protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
SubdomainMapping typeAnnotation = AnnotationUtils.findAnnotation(
handlerType, SubdomainMapping.class);
return createCondition(typeAnnotation);
}
#Override
protected RequestCondition<?> getCustomMethodCondition(Method method) {
SubdomainMapping methodAnnotation = AnnotationUtils.findAnnotation(
method, SubdomainMapping.class);
return createCondition(methodAnnotation);
}
private RequestCondition<?> createCondition(SubdomainMapping accessMapping) {
return (accessMapping != null) ? new SubdomainRequestCondition(
accessMapping.tld(), accessMapping.value()) : null;
}
}
Instalation
IMPORTANT: So far, it is not possible to use this solution with XML element
<mvc:annotation-driven />, see JIRA https://jira.springsource.org/browse/SPR-9344 for explanation
You have to register custom MappingHandler bean, pointing at this custom implementation SubdomainRequestMappingHandlerMapping class
You have to set it's order to be lower than default RequestMappingHandlerMapping
OR
Replace the registered RequestMappingHandlerMapping (possibly on order=0)
For more wide explanation on implementing this solution, see the related github project

That's correct, but that would be too complicated. You'd better check the Host header, whether it contains a given subdomain.
But you should not really need this more than once or twice, so you can also do it manually in the method body. If you really need it in many places, it would be an odd requirement.

Related

Custom AOP spring boot annotation with ribbon client blocking api call with return "1"

I have really poor experience with ribbon/eureka so forgive me if this is a stupid question:
I have two different microservice both connected to a discovery server, the first one calls the second using a custom annotation that sends a request using rest template.
Custom annotation name is PreHasAuthority
Controller :
#PreHasAuthority(value="[0].getProject()+'.requirements.update'")
#PostMapping(CREATE_UPDATE_REQUIREMENT)
public ResponseEntity<?> createUpdateRequirement(#Valid #RequestBody RequirementDTO requirementDTO
, HttpServletRequest request, HttpServletResponse response) {
return requirementService.createUpdateRequirement(requirementDTO, request, response);
}
Annotation interface :
import java.lang.annotation.*;
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface PreHasAuthority {
String value();
}
Annotation implementation:
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;
import netcomgroup.eu.service.AuthenticationService;
#Aspect
#Component
public class PreHasAuthorityServiceAspect {
#Autowired
private AuthenticationService authenticationService;
#Around(value = "#annotation(PreHasAuthority)")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
PreHasAuthority preHasAuthority = method.getAnnotation(PreHasAuthority.class);
Object[] args = joinPoint.getArgs();
String permission = preHasAuthority.value();
ExpressionParser elParser = new SpelExpressionParser();
Expression expression = elParser.parseExpression(permission);
String per = (String) expression.getValue(args);
String token =null;
for(Object o : args) {
if(o instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest)o;
token=request.getHeader("X-Auth");
break;
}
}
if(token==null) {
throw new IllegalArgumentException("Token not found");
}
boolean hasPerm = authenticationService.checkPermission(per,token);
if(!hasPerm)
throw new Exception("Not Authorized");
}
}
My Ribbon configuration
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
public class RibbonConfiguration {
#Autowired
IClientConfig config;
#Bean
public IRule ribbonRule(IClientConfig config) {
return new RoundRobinRule();
}
}
Eureka config in application properties
#Eureka config
eureka.client.serviceUrl.defaultZone= http://${registry.host:localhost}:${registry.port:8761}/eureka/
eureka.client.healthcheck.enabled= true
eureka.instance.leaseRenewalIntervalInSeconds= 10
eureka.instance.leaseExpirationDurationInSeconds= 10
by calling the api from postman request is sendend correctly to the second microservice and i'm certain the return is "true".
After that the request stops before entering the createUpdateRequirement method and returns '1' as postman body response. No error of sort is provided.
My guess is that the problem resides within the custom annotation, cause when i remove the annotation the api call works perfectly, but i cannot understand the problem as it seems all setted up correctly to me.
Your #Around advice never calls joinPoint.proceed(). Hence, the intercepted target method will never be executed.
The second problem is that your advice method returns void, i.e. it will never match any method returning another type such as the ResponseEntity<?> createUpdateRequirement(..) method.
Besides, around is a reserved keyword in native AspectJ syntax. Even though it might work in annotation-driven syntax, you ought to rename your advice method to something else like aroundAdvice or interceptPreHasAuthority - whatever.
Please do read an AspectJ or Spring AOP tutorial, especially the Spring manual's AOP chapter. 😉

#PropertySource(factory=...) breaks #SpringBootTest when loading META-INF/build-info.properties

I am trying to setup a custom #ConfigurationProperties class loaded from a HOCON syntax .conf file.
I have a Class annotated with #PropertySource(factory=TypesafePropertySourceFactory.class, value = "classpath:app.conf")
#Configuration
#ConfigurationProperties(value = "app.server")
#PropertySource(factory = TypesafePropertySourceFactory.class, value = "classpath:app.conf")
public class ServerProperties {
public int port;
}
and a simple test class:
#SpringBootTest
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SomeTest {
#Test
public void someCoolTest() {/* ... */}
// ...
}
When i run my junit test runner, i get the following error:
Caused by: com.typesafe.config.ConfigException$BadPath: path parameter: Invalid path 'spring.info.build.location:classpath:META-INF/build-info.properties': Token not allowed in path expression: ':' (you can double-quote this token if you really want it here)
at com.typesafe.config.impl.PathParser.parsePathExpression(PathParser.java:155) ~[config-1.4.0.jar:1.4.0]
at com.typesafe.config.impl.PathParser.parsePathExpression(PathParser.java:74) ~[config-1.4.0.jar:1.4.0]
at com.typesafe.config.impl.PathParser.parsePath(PathParser.java:61) ~[config-1.4.0.jar:1.4.0]
...
If i uncomment the #PropertySource line on the ServerProperties class, the tests proceed normally. It seems strange to me that my custom PropertySourceFactory gets in the way of the default .properties file resolution process.
PropertySource and Factory classes
// TypesafeConfigPropertySource.java
import com.typesafe.config.Config;
import org.springframework.core.env.PropertySource;
public class TypesafeConfigPropertySource extends PropertySource<Config> {
public TypesafeConfigPropertySource(String name, Config source) {
super(name, source);
}
#Override
public Object getProperty(String path) {
if (source.hasPath(path)) {
return source.getAnyRef(path);
}
return null;
}
}
// TypesafePropertySourceFactory.java
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.Objects;
public class TypesafePropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Config config = ConfigFactory.load(Objects.requireNonNull(resource.getResource().getFilename())).resolve();
String safeName = name == null ? "typeSafe" : name;
return new TypesafeConfigPropertySource(safeName, config);
}
}
Am I missing something fundamental about configuring custom property resource factories, or is this a bug?
Versions
Spring boot 2.3.4
Junit Jupiter 5.6.2
Maybe you can also solve it with the use of a ContextInitializer as suggested in the answer here:
Spring Environment backed by Typesafe Config
TL;DR
Return null if you cannot process the path in your custom impl
public class TypesafeConfigPropertySource extends PropertySource<Config> {
// ...
#Override
public Object getProperty(String path) {
try {
if (source.hasPath(path)) {
return source.getAnyRef(path);
}
} catch(ConfigException.BadPath ignore) {
}
return null;
}
// ...
}
Explanation
I am making educated guesses, but functionally this appears supported by the way the code behaves
the most likely scenario here is the resolution order will consider our custom implementation before any default implementation. The method used in our implementation will error out with any path containing a ":" and "[" as the error occurs in the check for the path's existence.
I'm simply wrapping the BadPath exception in order to catch any problem and then returning null to signify no match.

springdoc-openapi apply default global SecurityScheme possible?

I have the following SecurityScheme definition using springdoc-openapi for java SpringBoot RESTful app:
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("bearer-jwt",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")
.in(SecurityScheme.In.HEADER).name("Authorization")))
.info(new Info().title("App API").version("snapshot"));
}
Is it possible to apply it globally to all paths, without having to go and add #SecurityRequirement annotations to #Operation annotation everywhere in the code?
If it is, how to add exclusions to unsecured paths?
Yes, you can do it in the same place calling addSecurityItem:
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("bearer-jwt",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")
.in(SecurityScheme.In.HEADER).name("Authorization")))
.info(new Info().title("App API").version("snapshot"))
.addSecurityItem(
new SecurityRequirement().addList("bearer-jwt", Arrays.asList("read", "write")));
}
Global security schema can be overridden by a different one with the #SecurityRequirements annotation. Including removing security schemas for an operation. For example, we can remove security for registration path.
#SecurityRequirements
#PostMapping("/registration")
public ResponseEntity post(#RequestBody #Valid Registration: registration) {
return registrationService.register(registration);
}
While still keeping security schemas for other APIs.
Old answer (Dec 20 '19):
Global security schema can be overridden by a different one with the #SecurityRequirements annotation. but it cannot be removed for unsecured paths. It is acctualy missing fueature in the springdoc-openapi, OpenAPI standard allows it. See disable global security for particular operation
There is a workaround though. The springdoc-openapi has a concept of an OpenApiCustomiser which can be used to intercept generated schema. Inside the customizer, an operation can be modified programmatically. To remove any inherited security, the field security needs to be set to an empty array. The logic may be based on any arbitrary rules e.g operation name. I used tags.
The customizer:
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import org.springdoc.api.OpenApiCustomiser;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
#Component
public class SecurityOverrideCustomizer implements OpenApiCustomiser {
public static final String UNSECURED = "security.open";
private static final List<Function<PathItem, Operation>> OPERATION_GETTERS = Arrays.asList(
PathItem::getGet, PathItem::getPost, PathItem::getDelete, PathItem::getHead,
PathItem::getOptions, PathItem::getPatch, PathItem::getPut);
#Override
public void customise(OpenAPI openApi) {
openApi.getPaths().forEach((path, item) -> getOperations(item).forEach(operation -> {
List<String> tags = operation.getTags();
if (tags != null && tags.contains(UNSECURED)) {
operation.setSecurity(Collections.emptyList());
operation.setTags(filterTags(tags));
}
}));
}
private static Stream<Operation> getOperations(PathItem pathItem) {
return OPERATION_GETTERS.stream()
.map(getter -> getter.apply(pathItem))
.filter(Objects::nonNull);
}
private static List<String> filterTags(List<String> tags) {
return tags.stream()
.filter(t -> !t.equals(UNSECURED))
.collect(Collectors.toList());
}
}
Now we can add #Tag(name = SecurityOverrideCustomizer.UNSECURED) to unsecured methods:
#Tag(name = SecurityOverrideCustomizer.UNSECURED)
#GetMapping("/open")
#ResponseBody
public String open() {
return "It works!";
}
Please bear in mind that it is just a workaround. Hopefully, the issue will be resolved in the next springdoc-openapi versions (at the time of writing it the current version is 1.2.18).
For a working example see springdoc-security-override-fix
Tested with v1.2.29 of springdoc-openapi: Its possible to disable security for particular Endpoint using: #SecurityRequirements
#GetMapping("/open")
#ResponseBody
#SecurityRequirements
public String open() {
return "It works!";
}
For older versions, for example tested with v1.2.28 using OperationCustomizer:
public static final String UNSECURED = "security.open";
#Bean
public OperationCustomizer customize() {
return (Operation operation, HandlerMethod handlerMethod) -> {
List<String> tags = operation.getTags();
if (tags != null && tags.contains(UNSECURED)) {
operation.setSecurity(Collections.emptyList());
operation.setTags(tags.stream()
.filter(t -> !t.equals(UNSECURED))
.collect(Collectors.toList()));
}
return operation;
};
}
According to Documentation of springdoc, for new versions you can do this
Add the #SecurityRequirement for the protected route like
#PostMapping(value = "/example")
#SecurityRequirement(name = "bearer-key")
public ResponseEntity<Object> exampleHandler() {
/// logic here
}
and then in your Security config class add
#Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("bearer-key",
new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT")));
}
Here is the link to the docs

Support only a limited number of languages

My API uses Jersey 2, and now I want to support internationalization. I understand my client should specify the Accept-Language parameter, but I want to understand how to handle it properly.
Let's assume that my API should handle only FRENCH and ENGLISH languages. I know I can retrieve the preferred locale with the following code:
#GET
#Path("a-path")
public Response doSomething(#Context HttpServletRequest request) {
Locale locale = request.getLocale();
// ...
}
The problem is when the preferred locale isn't supported by my API. Let's say that my client send me Accept-Language: da, en-gb;q=0.8, en;q=0.7, according to w3c, it basically means: "I prefer Danish, but will accept British English and other types of English.". Since the preferred locale only return the most expected locale, is there a way to select the first supported language by my API? I would like to handle it in one place (i.e. in Filters) and not in every resources.
One to get the locales is to use HttpHeaders#getAcceptableLanguages().
Get a list of languages that are acceptable for the response.
If no acceptable languages are specified, a read-only list containing a single wildcard Locale instance (with language field set to "*") is returned.
Returns:
a read-only list of acceptable languages sorted according to their q-value, with highest preference first.
You can inject HttpHeaders pretty much anywhere, using #Context
public Response doSomething(#Context HttpHeaders headers) {
List<Locale> langs = headers.getAcceptableLanguages();
If you wanted to get the list in a filter, you can also get the list list of locales from the ContainerRequestContext
#Override
public void filter(ContainerRequestContext requestContext) throw .. {
List<Locales> langs = requestContext.getAcceptableLanguages();
}
If you wanted to use the Locale in the resource method, but didn't want to do all the locale "resolving" in the method, you can use some dependency injection, and create a Factory, where you can inject he HttpHeaders and resolve the locale there
See Also: Dependency injection with Jersey 2.0
Below is a complete test case example using a combination of last two points I mentioned about using a filter and dependency injection along a Factory, so that you can just inject the resolved Locale into the resource method. The example uses a dummy locale resolver that only allows english. After we resolve the locale, we set it into a request context property, and retrieve from inside the Factory so that we can inject it into the resource method
#GET
public String get(#Context Locale locale) {
return locale.toString();
}
See Also: How to inject an object into jersey request context?
Let me know if there is anything else you would like me to explain about the example
import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.process.internal.RequestScoped;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
/**
* Stack Overflow question https://stackoverflow.com/q/36871274/2587435
*
* Run this like any other JUnit test. Only one required test dependency:
*
* <dependency>
* <groupId>org.glassfish.jersey.test-framework.providers</groupId>
* <artifactId>jersey-test-framework-provider-inmemory</artifactId>
* <version>${jersey2.version}</version>
* </dependency>
*
* #author Paul Samsotha
*/
public class AcceptLanguageTest extends JerseyTest {
#Path("language")
public static class TestResource {
#GET
public String get(#Context Locale locale) {
return locale.toString();
}
}
public static interface LocaleResolver {
Locale resolveLocale(List<Locale> locales);
}
// Note: if you look in the javadoc for getAcceptableLanguages()
// you will notice that it says if there is not acceptable language
// specified, that there is a default single wildcard (*) locale.
// So this implementation sucks, as it doesn't check for that.
// You will want to make sure to do so!
public static class DefaultLocaleResolver implements LocaleResolver {
#Override
public Locale resolveLocale(List<Locale> locales) {
if (locales.contains(Locale.ENGLISH)) {
return Locale.ENGLISH;
}
return null;
}
}
#Provider
#PreMatching
public static class LocaleResolverFilter implements ContainerRequestFilter {
static final String LOCALE_PROPERTY = "LocaleResolverFilter.localProperty";
#Inject
private LocaleResolver localeResolver;
#Override
public void filter(ContainerRequestContext context) throws IOException {
List<Locale> locales = context.getAcceptableLanguages();
Locale locale = localeResolver.resolveLocale(locales);
if (locale == null) {
context.abortWith(Response.status(Response.Status.NOT_ACCEPTABLE).build());
return;
}
context.setProperty(LOCALE_PROPERTY, locale);
}
}
public static class LocaleFactory implements Factory<Locale> {
#Context
private ContainerRequestContext context;
#Override
public Locale provide() {
return (Locale) context.getProperty(LocaleResolverFilter.LOCALE_PROPERTY);
}
#Override
public void dispose(Locale l) {}
}
#Override
public ResourceConfig configure() {
return new ResourceConfig(TestResource.class)
.register(LocaleResolverFilter.class)
.register(new AbstractBinder() {
#Override
protected void configure() {
bindFactory(LocaleFactory.class)
.to(Locale.class).in(RequestScoped.class);
bind(DefaultLocaleResolver.class)
.to(LocaleResolver.class).in(Singleton.class);
}
})
.register(new LoggingFilter(Logger.getAnonymousLogger(), true));
}
#Test
public void shouldReturnEnglish() {
final String accept = "da, en-gb;q=0.8, en;q=0.7";
final Response response = target("language").request()
.acceptLanguage(accept)
.get();
assertThat(response.readEntity(String.class), is("en"));
}
#Test
public void shouldReturnNotAcceptable() {
final String accept = "da";
final Response response = target("language").request()
.acceptLanguage(accept)
.get();
assertThat(response.getStatus(), is(Response.Status.NOT_ACCEPTABLE.getStatusCode()));
}
}
The JAX-RS API allows you to select a Locale with the Request.selectVariant(List) method.
Try the following code in either the REST handler or a CDI bean :
import javax.ws.rs.core.Variant;
import javax.ws.rs.core.Request;
#Context
private Request req;
private Locale getResponseLocale(boolean throwIfNoneMatch) throws NotAcceptableException{
// Put your supported languages here
List<Variant> langVariants = Variant.languages(
new Locale("da"),
new Locale("en-gb"),
Locale.getDefault()).build();
Locale locale = Locale.getDefault();
Variant selectVariant = this.req.selectVariant(langVariants);
if (selectVariant != null) {
locale = selectVariant.getLanguage();
} else if (throwIfNoneMatch) {
throw new NotAcceptableException(Response.notAcceptable(langVariants).build());
}
return locale;
}
See also The Java EE Tutorial : Runtime Content Negotiation.

How would I write an automated check that every parameter has a specific annotation?

I'm writing a Rest API and my automated tests are calling the class directly without deploying the to the server. As an example, I am testing this method:
#GET
#Path("/{referenceId}")
#Produces("application/json")
public String findByReferenceId(#PathParam("referenceId") String referenceId,
String view) {
My tests are checking that the logic works and they pass. But this code has a bug: I forgot to put a #QueryParam annotation on that view parameter. So this code works when tested, but if you try to use this resource on the deployed app, the view parameter will never be settable.
There are many ways I can solve this, but my current preference is to somehow write an automated check that if a method has a #Path annotation, then every parameter must have either a #PathParam, a #QueryParam or whatever other valid annotation can be there.
I prefer this over a new end-to-end test, because my other tests are already covering 95% of that logic. I just don't know how to automate this check. I'm using Maven and CXF (which means I'm using Spring). I'm hoping there's a plugin that can be configured to do this.
Something I just realized: It's valid to have a single parameter without an annotation. When you do this, jax-rs sets it to the entity you pass in. I'm not sure how to deal with this scenario. I could create my own custom annotation called #Payload and tell people to use it, but something seems wrong about that.
Here's my solution. In the end, I decided to create a #RawPayload annotation. Otherwise, I can't know if the missing annotation is intentional or not. Here's where I got the Reflections class: https://code.google.com/p/reflections/
import org.junit.Test;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import javax.ws.rs.Path;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Set;
import static org.junit.Assert.assertTrue;
...
#Test
public void testAllParametersAreAnnotated() throws Exception {
String message = "You are missing a jax-rs annotation on a method's parameter: ";
Reflections reflections = new Reflections("package.for.my.services", new MethodAnnotationsScanner());
Set<Method> resourceMethods = reflections.getMethodsAnnotatedWith(Path.class);
assertTrue(resourceMethods.size() > 0);
for (Method resourceMethod : resourceMethods) {
for (int i = 0; i < resourceMethod.getGenericParameterTypes().length; i++) {
Annotation[] annotations = resourceMethod.getParameterAnnotations()[i];
boolean annotationExists = annotations.length > 0;
assertTrue(message +
resourceMethod.getDeclaringClass().getCanonicalName() +
"#" +
resourceMethod.getName(),
annotationExists && containsJaxRsAnnotation(annotations));
}
}
}
private boolean containsJaxRsAnnotation(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof RawPayload) {
return true;
}
if (annotation.annotationType().getCanonicalName().startsWith("javax.ws.rs")) {
return true;
}
}
return false;
}
Here's my annotation:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* I'm creating this marker so that we can put it on raw payload params. This is normally unnecessary,
* but it lets me write a very useful automated test.
*/
#Retention(RetentionPolicy.RUNTIME)
public #interface RawPayload {
}

Categories

Resources