Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Spring property placeholders with Jersey @Path and @ApplicationPath

I use Jersey and Spring in my project. 'jersey-spring3' is used for integration between them. I would like to make my resource classes more flexible and use properties inside @Path annotations, like:

@Path("${some.property}/abc/def")

But Spring can't inject some.property to Jersey's annotations @Path and @ApplicationPath.

Is there any way to have some configurable (using property files) value inside @Path value of Jersey's resource?

(I realize that it would be easier to replace Jersey by Spring MVC, but in my case unfortunately I don't have this choice.)

like image 551
XZen Avatar asked Jan 22 '16 09:01

XZen


1 Answers

So here's half the answer (or maybe a full answer depending on how important resolving @ApplicationPath is to you).

To understand the below solution, you should first understand a little about Jersey's internals. When we load our application, Jersey builds a model of all the resources. All the information for the resource are encapsulated in this model. Jersey uses this model for processing requests, rather than try and process the resource on each request, it's quicker to keep all the information about the resource in a model, and process the model.

With this architecture, Jersey also allows us to build resources programmatically, using the same APIs that it uses internally to hold the model properties. Aside from just building resource models, we can also modify existing models, using a ModelProcessors.

In the ModelProcessor, we can inject Spring's PropertyResolver, and then programmatically resolve the placeholders, and replace the old resource model path, with the resolved one. For example

@Autowired
private PropertyResolver propertyResolver;

private ResourceModel processResourceModel(ResourceModel resourceModel) {
    ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
    for (final Resource resource : resourceModel.getResources()) {
        final Resource.Builder resourceBuilder = Resource.builder(resource);
        String resolvedResourcePath = processPropertyPlaceholder(resource);
        resourceBuilder.path(resolvedResourcePath);

        // handle child resources
        for (Resource childResource : resource.getChildResources()) {
            String resolvedChildPath = processPropertyPlaceholder(childResource);
            final Resource.Builder childResourceBuilder = Resource.builder(childResource);
            childResourceBuilder.path(resolvedChildPath);
                resourceBuilder.addChildResource(childResourceBuilder.build());
        }
        newResourceModelBuilder.addResource(resourceBuilder.build());
    }
    return newResourceModelBuilder.build();
}

private String processPropertyPlaceholder(Resource resource) {
    String ogPath = resource.getPath();
    return propertyResolver.resolvePlaceholders(ogPath);
}

As far as the resource model APIs are concerned

  • This is a Resource

    @Path("resource")
    public class SomeResource {
        @GET
        public String get() {}
    }
    

    Its resource methods that are not annotated with @Path are ResourceMethods

  • This is a child Resource of the above Resource because it is annotated with @Path.

    @GET
    @Path("child-resource")
    public String get() {}
    

This information should give you some idea about how the above implementation works.

Below is a complete test, using Jersey Test Framework. The following classpath properties file is used

app.properties

resource=resource
sub.resource=sub-resource
sub.resource.locator=sub-resource-locator

You can run the following like any other JUnit test.

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.ModelProcessor;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.PropertyResolver;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
 * Stack Overflow http://stackoverflow.com/q/34943650/2587435
 * 
 * Run it like any other JUnit test. Required dependencies are as follows:
 * 
 * <dependency>
 *     <groupId>org.glassfish.jersey.test-framework.providers</groupId>
 *     <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
 *     <version>2.22.1</version>
 *     <scope>test</scope>
 * </dependency>
 * <dependency>
 *     <groupId>org.glassfish.jersey.ext</groupId>
 *     <artifactId>jersey-spring3</artifactId>
 *     <version>2.22.1</version>
 *     <scope>test</scope>
 * </dependency>
 * <dependency>
 *     <groupId>commons-logging</groupId>
 *     <artifactId>commons-logging</artifactId>
 *     <version>1.1</version>
 *     <scope>test</scope>
 * </dependency>
 * 
 * @author Paul Samsotha
 */
public class SpringPathResolverTest extends JerseyTest {

    @Path("${resource}")
    public static class TestResource {

        @GET
        public String get() {
            return "Resource Success!";
        }

        @GET
        @Path("${sub.resource}")
        public String getSubMethod() {
            return "Sub-Resource Success!";
        }

        @Path("${sub.resource.locator}")
        public SubResourceLocator getSubResourceLocator() {
            return new SubResourceLocator();
        }

        public static class SubResourceLocator {

            @GET
            public String get() {
                return "Sub-Resource-Locator Success!";
            }
        }
    }

    @Configuration
    @PropertySource("classpath:/app.properties")
    public static class SpringConfig {
    }

    public static class PropertyPlaceholderPathResolvingModelProcessor
            implements ModelProcessor {

        @Autowired
        private PropertyResolver propertyResolver;

        @Override
        public ResourceModel processResourceModel(ResourceModel resourceModel,
                javax.ws.rs.core.Configuration configuration) {
            return processResourceModel(resourceModel);
        }

        @Override
        public ResourceModel processSubResource(ResourceModel subResourceModel,
                javax.ws.rs.core.Configuration configuration) {
            return subResourceModel;
        }

        private ResourceModel processResourceModel(ResourceModel resourceModel) {
            ResourceModel.Builder newResourceModelBuilder = new ResourceModel.Builder(false);
            for (final Resource resource : resourceModel.getResources()) {
                final Resource.Builder resourceBuilder = Resource.builder(resource);
                String resolvedResourcePath = processPropertyPlaceholder(resource);
                resourceBuilder.path(resolvedResourcePath);

                // handle child resources
                for (Resource childResource : resource.getChildResources()) {
                    String resolvedChildPath = processPropertyPlaceholder(childResource);
                    final Resource.Builder childResourceBuilder = Resource.builder(childResource);
                    childResourceBuilder.path(resolvedChildPath);
                    resourceBuilder.addChildResource(childResourceBuilder.build());
                }
                newResourceModelBuilder.addResource(resourceBuilder.build());
            }
            return newResourceModelBuilder.build();
        }

        private String processPropertyPlaceholder(Resource resource) {
            String ogPath = resource.getPath();
            return propertyResolver.resolvePlaceholders(ogPath);
        }
    }

    @Override
    public ResourceConfig configure() {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        return new ResourceConfig(TestResource.class)
                .property("contextConfig", ctx)
                .register(PropertyPlaceholderPathResolvingModelProcessor.class)
                .register(new LoggingFilter(Logger.getAnonymousLogger(), true));
    }

    @Test
    public void pathPlaceholderShouldBeResolved() {
        Response response = target("resource").request().get();
        assertThat(response.getStatus(), is(200));
        assertThat(response.readEntity(String.class), is(equalTo("Resource Success!")));
        response.close();

        response = target("resource/sub-resource").request().get();
        assertThat(response.getStatus(), is(200));
        assertThat(response.readEntity(String.class), is(equalTo("Sub-Resource Success!")));
        response.close();

        response = target("resource/sub-resource-locator").request().get();
        assertThat(response.getStatus(), is(200));
        assertThat(response.readEntity(String.class), is(equalTo("Sub-Resource-Locator Success!")));
        response.close();
    }
}

Also now that I think about it, I can see a way to use resolve the @ApplicationPath, but it involves creating the Jersey servlet container programmatically in a Spring WebAppInitializer. Honestly, I think it would be more trouble than it's worth. I'd just suck it up, and leave the @ApplicationPath as a static string.


UDPATE

If you are using Spring boot, then the application path is definitely configurable, through the spring.jersey.applicationPath property. The way Spring boot loads up Jersey is pretty much the idea I had in mind with the above paragraph, where you create the Jersey servlet container yourself, and set the servlet mapping. This is how it is configurable with Spring Boot.

like image 85
Paul Samsotha Avatar answered Sep 29 '22 08:09

Paul Samsotha