REST Custom Context Providers

So a question came up today how to access the current user as part of a REST method body.

My friend, Andre Fabbro, was trying to build out the following application:

@ApplicationPath("/myapp")
@Component(immediate = true, service = Application.class)
public class MyApplication extends Application {

    @GET
    @Path("/whoami")
    @Produces(MediaType.APPLICATION_JSON)
    public String getUserFullName() {

        User user = ????;

        return user.getFullName();
    }
}

He was stuck trying to get the current user in order to finish the whoami handler.

So, being a long-time Liferay guy, I fell back on what I knew, and I pointed him towards the PrincipalThreadLocal and the getName() method to get the current user id.  Of course ThreadLocals kind of smell, they're almost like global variables, but I knew it would work.

My other friend, Rafael Oliveira, showed us both up and introduced me to the new concept of a custom context provider. You see, he knew that sometime soon a new module, com.liferay.portal.workflow.rest was coming and it was going to bring with it a new class, com.liferay.portal.workflow.rest.internal.context.provider.UserContextProvider. He did us one better by providing an implementation of Andre's app using @Context and the new UserContextProvider:

import javax.ws.rs.core.Context;

@ApplicationPath("/myapp")
@Component(immediate = true, service = Application.class)
public class MyApplication extends Application {

    @GET
    @Path("/whoami")
    @Produces(MediaType.APPLICATION_JSON)
    public String getUserFullName(@Context User user) {
        return user.getFullName();
    }
}

I was kind of blown away having learned something completely new with DXP and I needed to know more.

Before going on, though, all credit for this blog post goes to Rafael, all I'm doing here is putting it to electronic paper for us all to use for Liferay REST application implementations.

Basic @Context Usage

So when you create a new module using "blade create -t rest myapp", BLADE is starting a new JAX-RS-based RESTful application that you can build and deploy as an OSGi module. Using JAX-RS standard conventions, you can build out your RESTful methods using common annotations and (hopefully) best practices.

JAX-RS actually provides the javax.ws.rs.core.Context annotation and is used to inject common servlet-based values. Using @Context, you can define a method parameter that is not part of the RESTful call but are injected by the JAX-RS framework, kind of like the automagic ServiceContext injection in ServiceBuilder remote services.

Out of the box, JAX-RS Context annotation supports injecting the following parameters in methods:

Type Description
javax.ws.rs.core.Application Provides access to metadata information on the JAX-RS application.
javax.ws.rs.core.UriInfo Provides access to application and request URI information.
javax.ws.rs.core.Request Provides access to the request used for the method.
javax.ws.rs.core.HttpHeaders Provides access to the HTTP header information for the request.
javax.ws.rs.core.SecurityContext Provides access to the security-related information for the request.
javax.ws.rs.ext.Providers Provides runtime lookup of provider instances.

To use these, you just add appropriately decorated parameters to the REST method. If necessary, we could easily add a method to the application above such as:

@GET
@Path("/neato")
@Produces(MediaType.APPLICATION_JSON)
public String getStuff(@Context Application app, @Context UriInfo uriInfo, @Context Request request,
        @Context HttpHeaders httpHeaders, @Context SecurityContext securityContext, @Context Providers providers) {
    ....
}

The above getStuff() method will be handling all requests to the /neato path, but all of the parameters are injected, none are provided in the URL or as parameters; they are injected automagically by JAX-RS.

Custom @Context Usage

So these types are really nice, but they really don't do anything for our Liferay integration. What would be really cool is if we could use @Context to inject some Liferay parameters.

And we can! As Rafael pointed out, there is a new module in the pipeline for workflow to invoke RESTful methods on the backend. The new module is the portal-workflow-rest project. I'm not sure, but I believe this is going to be part of the upcoming GA4 release, but don't hold me to that.

Once available, this project will provide three new types that can be injected into RESTful method parameters:

Type Description
com.liferay.portal.kernel.model.Company The Liferay Company associated with the request.
java.util.Locale The locale associated with the request.
com.liferay.portal.kernel.model.User The Liferay User associated with the request.

So, like the out of the box parameters, we could extend our getStuff() method with these parameters too:

@GET
@Path("/neato")
@Produces(MediaType.APPLICATION_JSON)
public String getStuff(@Context Application app, @Context UriInfo uriInfo, @Context Request request,
        @Context HttpHeaders httpHeaders, @Context SecurityContext securityContext, @Context Providers providers,
        @Context Company company, @Context Locale locale, @Context User user) {
    ....
}

Just pick from all of these different available types to get the data you need and run with it.

Remember these will not be available in GA3 nor in DXP just yet - I'm sure they'll make it in soon, but I'm not aware of the schedule for either product lines.

Writing Custom Context Providers

So to me, the biggest value of this new module is this package: https://github.com/liferay/liferay-portal/tree/master/modules/apps/forms-and-workflow/portal-workflow/portal-workflow-rest/src/main/java/com/liferay/portal/workflow/rest/internal/context/provider

Why? Because they expose how we can write our own custom context provider implementations so we can inject custom parameters into REST methods.

Say, for example, that we want to inject a ServiceContext instance. I'm not sure if the portal source already has one of these fellas, but if so let's pretend it doesn't exist and we want to write our own. Where are we going to start?

So first you need a project, we'll create a blade workspace:

blade init custom-context-provider

We also need a new module to develop, so we'll change to the custom-context-provider/modules directory to create an initial module:

blade create -t api -p com.dnebinger.rest.internal.context.provider service-context-context-provider

This will give us a nearly empty API module. We'll end up cleaning out most of the generated files, but we will end up with the com.dnebinger.rest.internal.context.provider.ServiceContextContextProvider class:

package com.dnebinger.rest.internal.context.provider;

import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.service.ServiceContext;
import com.liferay.portal.kernel.service.ServiceContextFactory;
import org.apache.cxf.jaxrs.ext.ContextProvider;
import org.apache.cxf.message.Message;
import org.osgi.service.component.annotations.Component;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.ext.Provider;

/**
 * class ServiceContextContentProvider: A custom context provider for ServiceContext instantiation.
 *
 * @author dnebinger
 */
@Component(immediate = true, service = ServiceContextContentProvider.class)
@Provider
public class ServiceContextContentProvider implements ContextProvider {
	/**
	 * Creates the context instance
	 *
	 * @param message the current message
	 * @return the context
	 */
	@Override
	public ServiceContext createContext(Message message) {
		ServiceContext serviceContext = null;

		// get the current HttpServletRequest for building the service context instance.
		HttpServletRequest request = (HttpServletRequest) message.getContextualProperty(PROPKEY_HTTP_REQUEST);

		try {
			// now we can create a service context
			serviceContext = ServiceContextFactory.getInstance(request);

			// done!
		} catch (PortalException e) {
			_log.warn("Failed creating service context: " + e.getMessage(), e);
		}

		// return the new instance.
		return serviceContext;
	}

	private static final String PROPKEY_HTTP_REQUEST = "HTTP.REQUEST";

	private static final Log _log = LogFactoryUtil.getLog(ServiceContextContentProvider.class);
}

So this is pretty much the whole module. Easy, huh?

Conclusion

Now that we can create custom context providers, we can use this one for example in the original code:

@ApplicationPath("/myapp")
@Component(immediate = true, service = Application.class)
public class MyApplication extends Application {

    @GET
    @Path("/whoami")
    @Produces(MediaType.APPLICATION_JSON)
    public String getUserFullName(@Context ServiceContext serviceContext) {

        User user = _userLocalService.fetchUser(serviceContext.getUserId());

        return user.getFullName();
    }

    @Reference
    private UserLocalService _userLocalService;
}

These custom context providers become the key for being able to create and inject non-REST parameters into your REST methods.

Check out the code from GitHub: https://github.com/dnebing/custom-context-provider

Enjoy!

Blogs
Hello David,

thanks for the post, the functionality you present looks really great. However I have some problems making it work. I've tried implement my custom context provider, I tried to copy-paste User provider from Liferay GIT and I have also tried building and running your project. Every time without any success.

I've tried debugging #createContext method of providers but the execution never got there. Is there anything else I have to do with my environment to make Context Providers work? I'm running DXP SP6 version.

Thank you,
Martin

Unfortunately, one step is missing. I know your post is already 2 years ago but for the potential readers, following step is missing

 

@Override         public Set<Object> getSingletons() {                 Set<Object> singletons = new HashSet<>();

                singletons.add(_companyContextProvider);                 singletons.add(_localeContextProvider);                 singletons.add(_userContextProvider);                 singletons.add(_workflowListedTaskResource);                 singletons.add(_workflowTaskResource);

                return singletons;         }

 

Check WorkflowJaxRsApplication.java in the workflow REST module.

 

I wish I could enable this provider without using singletons.add() and using only OSGI annotations.

 

Thanks, Eric. Yeah, this post isn't just two years old, it was also written for Liferay 7.0 IIRC. It doesn't surprise me that the code needed a little bit of TLC.  

 

 

 

Thanks for the update!