Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jersey custom method parameter injection with inbuild injection

Hello I am building an application using dropwizard, that is using jersey 2.16 internally as REST API framework.

For the whole application on all resource methods I need some information so to parse that information I defined a custom filter like below

@java.lang.annotation.Target(ElementType.PARAMETER)
@java.lang.annotation.Retention(RetentionPolicy.RUNTIME)
public @interface TenantParam {
}

The tenant factory is defined below

public class TenantFactory implements Factory<Tenant> {

    private final HttpServletRequest request;
    private final ApiConfiguration apiConfiguration;

    @Inject
    public TenantFactory(HttpServletRequest request, @Named(ApiConfiguration.NAMED_BINDING) ApiConfiguration apiConfiguration) {
        this.request = request;
        this.apiConfiguration = apiConfiguration;
    }

    @Override
    public Tenant provide() {
        return null;
    }

    @Override
    public void dispose(Tenant tenant) {

    }
}

I haven't actually implemented the method but structure is above. There is also a TenantparamResolver

public class TenantParamResolver implements InjectionResolver<TenantParam> {

    @Inject
    @Named(InjectionResolver.SYSTEM_RESOLVER_NAME)
    private InjectionResolver<Inject> systemInjectionResolver;

    @Override
    public Object resolve(Injectee injectee, ServiceHandle<?> serviceHandle) {
        if(Tenant.class == injectee.getRequiredType()) {
            return systemInjectionResolver.resolve(injectee, serviceHandle);
        }
        return null;
    }

    @Override
    public boolean isConstructorParameterIndicator() {
        return false;
    }

    @Override
    public boolean isMethodParameterIndicator() {
        return true;
    }
}

Now in my resource method I am doing like below

@POST
@Timed
public ApiResponse create(User user, @TenantParam Tenant tenant) {
    System.out.println("resource method invoked. calling service method");
    System.out.println("service class" + this.service.getClass().toString());
    //DatabaseResult<User> result = this.service.insert(user, tenant);
    //return ApiResponse.buildWithPayload(new Payload<User>().addObjects(result.getResults()));
    return null;
}

Here is how I am configuring the application

@Override
public void run(Configuration configuration, Environment environment) throws Exception {
    // bind auth and token param annotations
    environment.jersey().register(new AbstractBinder() {
        @Override
        protected void configure() {
            bindFactory(TenantFactory.class).to(Tenant.class);
            bind(TenantParamResolver.class)
                .to(new TypeLiteral<InjectionResolver<TenantParam>>() {})
                .in(Singleton.class);
        }
    });
}

The problem is during application start I am getting below error

WARNING: No injection source found for a parameter of type public void com.proretention.commons.auth.resources.Users.create(com.proretention.commons.api.core.Tenant,com.proretention.commons.auth.model.User) at index 0.

and there is very long stack error stack and description

Below is the declaration signature of user pojo

public class User extends com.company.models.Model {

No annotations on User class. Model is a class that defines only single property id of type long and also no annotations on model class

When I remove the User parameter from above create resource method it works fine and when I removed TenantParam it also works fine. The problem only occurs when I use both User and TenantParam

  1. What I am missing here ? how to resolve this error ?

EDITED

I just tried with two custom method param injection, that is also not working

@POST
@Path("/login")
@Timed
public void validateUser(@AuthParam AuthToken token, @TenantParam Tenant tenant) {


}
  1. What I am missing here ? Is this a restriction in jersey ?
like image 824
Syed Avatar asked Mar 19 '15 13:03

Syed


1 Answers

Method parameters are handled a little differently for injection. The component we need to implement for this, is the ValueFactoryProvider. Once you implement that, you also need to bind it in your AbstractBinder.

Jersey has a pattern that it follows for implementing the ValueFactoryProvider. This is the pattern used to handle parameters like @PathParam and @QueryParam. Jersey has a ValueFactoryProvider for each one of those, as well as others.

The pattern is as follows:

  1. Instead of implementing the ValueFactoryProvider directly, we extend AbstractValueFactoryProvider

    public static class TenantValueProvider extends AbstractValueFactoryProvider {
    
        @Inject
        public TenantValueProvider(MultivaluedParameterExtractorProvider mpep,
                               ServiceLocator locator) {
            super(mpep, locator, Parameter.Source.UNKNOWN);
        }
    
        @Override
        protected Factory<?> createValueFactory(Parameter parameter) {
            if (!parameter.isAnnotationPresent(TenantParam.class) 
                    || !Tenant.class.equals(parameter.getRawType())) {
                return null;
            }
            return new Factory<Tenant>() {
    
                @Override
                public Tenant provide() {
                    ...
                }
            };
        }
    

    In this component, it has a method we need to implement that returns the Factory that provides the method parameter value.

  2. The InjectionResolver is what is used to handle the custom annotation. With this pattern, instead of directly implementing it, as the OP has, we just extend ParamInjectionResolver passing in our AbstractValueFactoryProvider implementation class to super constructor

    public static class TenantParamInjectionResolver 
            extends ParamInjectionResolver<TenantParam> {
    
        public TenantParamInjectionResolver() {
            super(TenantValueProvider.class);
        }
    } 
    

And that's really it. Then just bind the two components

public static class Binder extends AbstractBinder {
    @Override
    public void configure() {
        bind(TenantParamInjectionResolver.class)
                .to(new TypeLiteral<InjectionResolver<TenantParam>>(){})
                .in(Singleton.class);
        bind(TenantValueProvider.class)
                .to(ValueFactoryProvider.class)
                .in(Singleton.class);
    }
}

Below is a complete test using Jersey Test Framework. The required dependencies are listed in the javadoc comments. You can run the test like any other JUnit test

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.logging.Logger;
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.core.Response;
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.filter.LoggingFilter;
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 org.junit.Test;
import static org.junit.Assert.assertEquals;

/**
 * Stack Overflow https://stackoverflow.com/q/29145807/2587435
 * 
 * Run this like any other JUnit test. Dependencies required are as the following
 * 
 *  <dependency>
 *      <groupId>org.glassfish.jersey.test-framework.providers</groupId>
 *      <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
 *      <version>2.22</version>
 *      <scope>test</scope>
 *  </dependency>
 *  <dependency>
 *      <groupId>org.glassfish.jersey.media</groupId>
 *      <artifactId>jersey-media-json-jackson</artifactId>
 *      <version>2.22</version>
 *      <scope>test</scope>
 *  </dependency>
 * 
 * @author Paul Samsotha
 */
public class TenantInjectTest extends JerseyTest {

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

    public static class User {
        public String name;
    }

    public static class Tenant {
        public String name;
        public Tenant(String name) {
            this.name = name;
        }
    }

    public static class TenantValueProvider extends AbstractValueFactoryProvider {

        @Inject
        public TenantValueProvider(MultivaluedParameterExtractorProvider mpep,
                                   ServiceLocator locator) {
            super(mpep, locator, Parameter.Source.UNKNOWN);
        }

        @Override
        protected Factory<?> createValueFactory(Parameter parameter) {
            if (!parameter.isAnnotationPresent(TenantParam.class) 
                    || !Tenant.class.equals(parameter.getRawType())) {
                return null;
            }
            return new AbstractContainerRequestValueFactory<Tenant>() {
                // You can @Inject things here if needed. Jersey will inject it.
                // for example @Context HttpServletRequest

                @Override
                public Tenant provide() {
                    final ContainerRequest request = getContainerRequest();
                    final String name 
                            = request.getUriInfo().getQueryParameters().getFirst("tenent");
                    return new Tenant(name);
                }
            };
        }

        public static class TenantParamInjectionResolver 
                extends ParamInjectionResolver<TenantParam> {

            public TenantParamInjectionResolver() {
                super(TenantValueProvider.class);
            }
        } 

        public static class Binder extends AbstractBinder {
            @Override
            public void configure() {
                bind(TenantParamInjectionResolver.class)
                        .to(new TypeLiteral<InjectionResolver<TenantParam>>(){})
                        .in(Singleton.class);
                bind(TenantValueProvider.class)
                        .to(ValueFactoryProvider.class)
                        .in(Singleton.class);
            }
        }
    }


    @Path("test")
    @Produces("text/plain")
    @Consumes("application/json")
    public static class TestResource {
        @POST
        public String post(User user, @TenantParam Tenant tenent) {
            return user.name + ":" + tenent.name;
        }
    }

    @Override
    public ResourceConfig configure() {
        return new ResourceConfig(TestResource.class)
                .register(new TenantValueProvider.Binder())
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true));
    }

    @Test
    public void shouldReturnTenantAndUserName() {
        final User user = new User();
        user.name = "peeskillet";
        final Response response = target("test")
                .queryParam("tenent", "testing")
                .request()
                .post(Entity.json(user));

        assertEquals(200, response.getStatus());
        assertEquals("peeskillet:testing", response.readEntity(String.class));
    }
}

See Also:

  • Jersey 2.x Custom Injection Annotation With Attributes
  • My Comment in the Dropwizard issue: "No injection source found for a parameter"
like image 199
Paul Samsotha Avatar answered Nov 09 '22 04:11

Paul Samsotha