2

Currently I'm rendering a command object in a MessageBodyReader but I'd like to be able to do this in a @BeanParam:

  1. Inject a field derived from the SecurityContext (Is there somewhere to hook in the conversion?).

  2. have a field inject that has been materialised by a MessageBodyReader.

Is this possible ?

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
Hassan Syed
  • 20,075
  • 11
  • 87
  • 171

1 Answers1

1

Note: Go Down to UPDATE. I guess it is possible to use @BeanParam. Though you need to inject the SecurityContext into the bean and extract the name info.


There's no way to achieve this with @BeanParam corrected. You could use a MessageBodyReader the way you are doing, but IMO that's more of a hack than anything. Instead, the way I would achieve it is to use the framework components the way they are supposed to be used, which involves custom parameter injection.

To achieve this, you need two things, a ValueFactoryProvider to provide parameter values, and a InjectionResolver with your own custom annotation. I won't do much explaining for the example below, but you can find a good explanation in

You can run the below example like any JUnit test. Everything is included into the one class. These are the dependencies I used.

<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>2.19</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>2.19</version>
    <scope>test</scope>
</dependency>

And here is the test

import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.security.Principal;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Entity;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.api.InjectionResolver;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.api.TypeLiteral;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory;
import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider;
import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider;
import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver;
import org.glassfish.jersey.server.model.Parameter;
import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider;
import org.glassfish.jersey.test.JerseyTest;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CustomInjectionTest extends JerseyTest {

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface CustomParam {
    }

    public static class CustomModel {

        public String name;
        public RequestBody body;
    }

    public static class RequestBody {

        public String message;
    }

    public static class CustomParamValueFactory
            extends AbstractContainerRequestValueFactory<CustomModel> {

        @Override
        public CustomModel provide() {
            ContainerRequest request = getContainerRequest();
            String name = request.getSecurityContext().getUserPrincipal().getName();
            RequestBody body = request.readEntity(RequestBody.class);
            CustomModel model = new CustomModel();
            model.body = body;
            model.name = name;
            return model;
        }
    }
    
    public static class CustomValueFactoryProvider extends AbstractValueFactoryProvider {
        
        @Inject
        public CustomValueFactoryProvider(MultivaluedParameterExtractorProvider multiProvider,
                                          ServiceLocator locator) {
            super(multiProvider, locator, Parameter.Source.UNKNOWN);
        }

        @Override
        protected Factory<?> createValueFactory(Parameter parameter) {
            if (CustomModel.class == parameter.getType()
                    && parameter.isAnnotationPresent(CustomParam.class)) {
                return new CustomParamValueFactory();
            }
            return null;
        }  
    }
    
    public static class CustomParamInjectionResolver extends ParamInjectionResolver<CustomParam> {
        
        public CustomParamInjectionResolver() {
            super(CustomValueFactoryProvider.class);
        }
    }
    
    private static class CustomInjectBinder extends AbstractBinder {

        @Override
        protected void configure() {
            bind(CustomValueFactoryProvider.class)
                    .to(ValueFactoryProvider.class)
                    .in(Singleton.class);
            bind(CustomParamInjectionResolver.class)
                    .to(new TypeLiteral<InjectionResolver<CustomParam>>(){})
                    .in(Singleton.class);
        } 
    }
    
    private static final String PRINCIPAL_NAME = "peeskillet";
    
    @PreMatching
    public static class SecurityContextFilter implements ContainerRequestFilter {

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            requestContext.setSecurityContext(new SecurityContext(){
                public Principal getUserPrincipal() {
                    return new Principal() { 
                        public String getName() { return PRINCIPAL_NAME; }
                    };
                }
                public boolean isUserInRole(String role) { return false; }
                public boolean isSecure() { return true;}
                public String getAuthenticationScheme() { return null; }
            });   
        }  
    }
    
    @Path("test")
    public static class TestResource {
        @POST
        @Produces(MediaType.TEXT_PLAIN)
        @Consumes(MediaType.APPLICATION_JSON)
        public String post(@CustomParam CustomModel model) {
            return model.name + ":" + model.body.message;
        }
    }
    
    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class)
                .register(SecurityContextFilter.class)
                .register(new CustomInjectBinder());
    }
    
    @Test
    public void should_return_name_with_body() {
        RequestBody body = new RequestBody();
        body.message = "Hello World";
        Response response = target("test").request()
                .post(Entity.json(body));
        assertEquals(200, response.getStatus());
        String responseBody = response.readEntity(String.class);
        assertEquals(PRINCIPAL_NAME + ":" + body.message, responseBody);
        System.out.println(responseBody);
    }
}

Note that I read the request body from the ContainerRequest inside the CustomParamValueFactory. It is the same RequestBody that I sent in JSON from the request in the @Test.


UPDATE

So to my surprise, it is possible to use @BeanParam. Here is the following bean I used to test

public static class CustomModel {

    @Context
    public SecurityContext securityContext;
    public RequestBody body;
}

public static class RequestBody {

    public String message;
}

The difference from the previous test is that instead of the name from the SecurityContext.Principal, we need to inject the entire SecurityContext. There's just no way for the inject to get the name from the Principal, So we will just do it manually.

The thing that surprised me the most though, is that we are able to inject the RequestBody entity. I didn't know this was possible.

Here is the complete test

import java.io.IOException;
import java.security.Principal;
import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Entity;
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.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CustomInjectTestTake2 extends JerseyTest {
    
    private static final String PRINCIPAL_NAME = "peeskillet";
    private static final String MESSAGE = "Hello World";
    private static final String RESPONSE = PRINCIPAL_NAME + ":" + MESSAGE;
    
    public static class CustomModel {

        @Context
        public SecurityContext securityContext;
        public RequestBody body;
    }

    public static class RequestBody {

        public String message;
    }
    
    @PreMatching
    public static class SecurityContextFilter implements ContainerRequestFilter {

        @Override
        public void filter(ContainerRequestContext requestContext) throws IOException {
            requestContext.setSecurityContext(new SecurityContext(){
                public Principal getUserPrincipal() {
                    return new Principal() { 
                        public String getName() { return PRINCIPAL_NAME; }
                    };
                }
                public boolean isUserInRole(String role) { return false; }
                public boolean isSecure() { return true;}
                public String getAuthenticationScheme() { return null; }
            });   
        }  
    }
    
    @Path("test")
    public static class TestResource {
        @POST
        @Produces(MediaType.TEXT_PLAIN)
        @Consumes(MediaType.APPLICATION_JSON)
        public String post(@BeanParam CustomModel model) {
            return model.securityContext.getUserPrincipal().getName() 
                    + ":"  + model.body.message;
        }
    }
    
    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class)
                .register(SecurityContextFilter.class);
    }
    
    @Test
    public void should_return_name_with_body() {
        RequestBody body = new RequestBody();
        body.message = "Hello World";
        Response response = target("test").request()
                .post(Entity.json(body));
        assertEquals(200, response.getStatus());
        String responseBody = response.readEntity(String.class);
        assertEquals(RESPONSE, responseBody);
        System.out.println(responseBody);
    }
}

See Also:

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Many thanks for the setting me straight :D I was originally going to use this approach (2 weeks ago) but I was quite early in the skilling up phase at the time and it seemed like a tangent or over-engineering with the possibility of me finding a more appropriate solution. – Hassan Syed Oct 27 '15 at 19:32
  • Yeah it does seem like a bit much, but this is actually how Jersey handles all of the `@BeanParam`, `@FormParam`, `@PathParam`, etc. So we could follow this pattern... or not :-) – Paul Samsotha Oct 27 '15 at 22:46
  • @HassanSyed Please see my UPDATE. I guess it _is_ possible with `@BeanParam` :-) – Paul Samsotha Oct 29 '15 at 04:59
  • Having a body in a BeanParam annotated class didn't work for me. body is not mapped; always null. – starkm Sep 16 '21 at 06:38