Expand my Community achievements bar.

SOLVED

How to get ResourceResolver in a background thread?

Avatar

Level 3

I'm working on a solution that receives an HTTP request containing a URL to a file, which I want to download and store in the JCR.

So, I have a servlet that receives the request. It spawns a thread so that I can do the download in the background, and then redirects to a confirmation page. This allows me to send the user on their way without waiting while I try to download the file.

I can download the file just fine, but I'm having trouble getting a usable ResourceResolver to store the file in the JCR from my thread.

At first, I simply referenced the request's ResourceResolver in the background thread:

Servlet:

    pubic void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)

    throws ServletException, IOException {

    ...

    signingProvider.getDocFromService(params, request.getResourceResolver());

    response.sendRedirect(confirmationPage);

}

And in the provider class:

    public void getDocFromService(Map<String, String> params, ResourceResolver resolver) {

        new Thread( new Runnable() {

            public void run() {

                Session session = null;

                if (resolver != null) {

                    session = resolver.adaptTo(Session.class);

                    Node root = session.getRootNode();

                    ...

                }

            }

but that didn't work. After reading up on resolvers vs threads, I thought I'd be better off creating a new Resolver instance, so I tried to inject a ResourceResolverFactory:

Servlet:

    signingProvider.getDocFromService(params);

Provider:

    public void getDocFromService(Map<String, String> params) {

        new Thread( new Runnable() {

            @Reference

            private ResourceResolverFactory resolverFactory;

           

            // security hole, fix later

            ResourceResolver resolver = resolverFactory.getAdministrativeResourceResolver(null);

            Session session = null;

            if (resolver != null) {

                session = resolver.adaptTo(Session.class);

                Node root = session.getRootNode();

                ...

            }

        }

but the ResourceResolverFactory is null, so I crash when asking it for a resolver. Apparently, no factory is getting injected into the @Reference

I'd really rather not do the work on the main thread; after I download the file I'm going to turn around and read it from the JCR and copy it elsewhere. Both of these operations could be slow or fail. I have a copy of the file at the original URL, so the end-user needn't care if my download/uploads had trouble. I just want to send them a confirmation so they can get on with business.

Any advice on how to get a ResourceResolver in a separate thread? (cross-posted to java - How to get ResourceResolver in a background thread? - Stack Overflow )

1 Accepted Solution

Avatar

Correct answer by
Level 3

Sorry for the delay, but I finally got it working. A few notes:

My initial design was never going to work. I tried to pass the servlet's request.getResourceResolver() down to my worker class and then reference it in the new Thread. But when the servlet then issues a redirect (or any other response) back to the client, the request, and therefore the ResourceResolver, is closed, and the thread throws an exception when trying to use the now-closed resolver.

My second attempt was close, but I had @Reference in the wrong place. Without an @Service annotation, my Runnable couldn't get wired up by the SCR machinery. However, when I moved the @Reference to be a class member of the worker class containing the method that spawns the thread, I'm able to use the factory to get a new resolver. So my code now looks like this:

@Component(metatype = true)

@Service

public class DocumentSigningProviderDocuSignImpl implements DocumentSigningProvider {

    @Reference

    private ResourceResolverFactory resolverFactory;

    @Override

    public void getDocFromService(Map<String, String> params) {

        new Thread( new Runnable() {

            public void run() {

                ResourceResolver resolver = resolverFactory.getAdministrativeResourceResolver(null);

                Session session = resolver.adaptTo(Session.class);  

which works as expected.

Everyone is correct in pointing out that using an AdministrativeResourceResolver is wrong; I even put a comment in my code to that effect:

// security hole, fix later

But that wasn't the problem I was trying to solve, and changing to a service account doesn't make the code work. Using an Admin resolver was just a quick-and-dirty way to get running; we (as I'm sure most AEM customers) have this throughout our codebase (I just counted 40 occurrences), and now that my code works as expected I'll go back and replace it with a Service account.

Thanks for the help and advice!

View solution in original post

10 Replies

Avatar

Level 3

valcohen

Your first approach is good and passing the ResourceResolver to a new Thread has no problem. In fact you can test by displaying the ID of the connected user ResourceResolver resolves for. In order to test that ResourceResolver is available in a new thread you can:

1. add a log message in your servlet prior to calling getDocFromService() Method

logger.info("Resolved user: " + request.getResourceResolver().getUserID());

2. add a log message inside the new thread

logger.info("resolved user inside new thread: " + resolver.getUserID());

Should you be running your local AEM and signed in as "admin" then you'll that's the user that's logged and accessing the repository. Nonetheless, once you deploy to a publish instances it's likely the servlet will be access by an anonymous user and therefore this line on your code "Node root = session.getRootNode();" will fail as the anonymous user does not have access to the root of the repository.

Actual Solution:

1. create a System User with proper write permission to the JCR where you'll be copying the downloaded file

2. Create a mapper for the bundleId, the subservice and the System user

3. get resourceResolver

         // prepare params to get system user

        Map<String, Object> serviceParams = new HashMap<String, Object>();

        serviceParams.put(ResourceResolverFactory.SUBSERVICE, "your subservice name");

        try {

          // get resource resolver for the system user set in user mapper

            ResourceResolver resourceResolver = resourceResolverFactory.

                                                                getServiceResourceResolver(serviceParams);

Avatar

Employee Advisor

Hi,

I don't understand your approach. According to my knowledge a HTTP request possesses a single HTTP response with a single statuscode. If you have a browser request you can either send back a redirect (HTTP statuscode 301) or initiate the download (sending 200 plus the binary as response body). You can't do both.

If you want the browser to do both things in parallel, you have to send 2 requests: One for the download and the other for the redirect. And very likely this is handled by the frontend, but not on the server side.

Jörg

Avatar

Level 3

Interesting, I'll give it a try and let you know how it works out.

Avatar

Level 3

I'm not downloading from the response; I'm downloading from a separate URL. The flow is:

  1. user opens a page with a form from my AEM site, and POSTs the form to my servlet
  2. servlet converts the form to PDF, sends it to docusign server-side, and gets back a DocuSign redirect URL
  3. servlet responds to the request from (1) with a redirect to the URL from (2)
  4. the redirect displays the DocuSign form-signing page. When the user completes it, that page GETs my servlet again, with an ID that lets me know where the signed doc is on DocuSign
  5. my servlet
    1. start a new thread to download the completed form from DocuSign, and
    2. I send a redirect response to the user, sending them to a form completion page served form AEM.

All this works as expected; even 5.1 (the download) works fine if I DL to my local filesystem. But I want to DL to the JCR, so I don't have t deal with writing to the AEM server's filesystem. I'm having trouble getting a ResourceResolver (in order to write to the JCR) on the background thread.

Avatar

Level 3

Julio TobarWell , I just tried, but immediately inside the run() method of my Runnable, when I test the resolver I get an exception that says

This makes sense to me -- I get the resolver from the request to my servlet. The servlet passes its resolver to my worker class (so far so good, it logs the admin user), but the worker class spawns a new thread and in its run() method the resolver is closed. I think that's by the time the Runnable executes run(), the servlet has already issued its redirect to the client, which completes the request/response cycle and the resources are cleaned up.

So I think I need my Runnable class to create its own Resolver. Injection didn't work; someone else suggested using an OSGi service as my Runnable, as a plain anonymous Java class doesn't work with the SCR annotations. So my next step is to convert from an anonymous class to an OSGI component and see if that will let me inject the ResourceResolverFactory.

Avatar

Level 3

valcohen​ I tested and it actually works so maybe there's a different object/method we're using. Here's code for the two classes I created. Can you please test on your end?

Servlet to capture request

package com.nnp.forums63.newthread;

import java.io.IOException;

import org.apache.felix.scr.annotations.sling.SlingServlet;

import org.apache.sling.api.servlets.SlingSafeMethodsServlet;

import org.apache.sling.api.SlingHttpServletRequest;

import org.apache.sling.api.SlingHttpServletResponse;

import org.apache.sling.api.resource.ResourceResolver;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

@SlingServlet (paths = "/bin/nnp-forums63/newthread")

public class NewThreadServlet extends SlingSafeMethodsServlet {

  /* globals */

  private Logger logger = LoggerFactory.getLogger(this.getClass());

 

  @Override

  public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)

                                                                                throws IOException {

    logger.info("doGet enters");

    /* prepare response */

    response.setHeader("Content-Type", "text/html");

   

    /* grab resourceResolver from request object */

    ResourceResolver resourceResolver = request.getResourceResolver();

    logger.info("Resolved user: " + resourceResolver.getUserID());

   

    /* call method that creates new Thread */

    new NewThread().getDocFromService(null, resourceResolver);

   

    /* return some response */

    response.getWriter().print("New Thread requested");

  }

}

Class that creates a new Thread and uses ResourceResolver

package com.nnp.forums63.newthread;

import java.util.Map;

import javax.jcr.Node;

import javax.jcr.RepositoryException;

import javax.jcr.Session;

import org.apache.sling.api.resource.ResourceResolver;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

public class NewThread {

  /* globals */

  private Logger logger = LoggerFactory.getLogger(this.getClass());

  /* ----------------------------

   * GET DOC FROM SERVICE

   * ---------------------------*/

  public void getDocFromService(Map<String, String> params, ResourceResolver resourceResolver) {

   

    logger.info("resolved user prior creating new thread: " + resourceResolver.getUserID());

    /* create new thread */

    new Thread( new Runnable() {

      public void run() {

        logger.info("resolved user inside new thread: " + resourceResolver.getUserID());

       

        /* grab Session from ResourceResolver */

        try {

          Session session = null;

          if (resourceResolver != null) {

              session = resourceResolver.adaptTo(Session.class);

              Node root = session.getRootNode();

          }

        } catch (RepositoryException e) {

          e.printStackTrace();

        }

      }

    });

  }

}

Avatar

Level 3

Julio Tobar Wow, you're fast! I've been playing with a couple of approaches and still no-go, but haven't tried your latest suggestion yet. I'm wrapping up work now -- we have a holiday weekend here -- so I'll get back to this Tuesday morning. I'll let you know how it works out -- thanks very much for all the help!

Avatar

Employee Advisor

Hi,

ok, now I get it. I was confused by the term "download" :-)

Your problem is, that the resource resolver attached to a request is managed by the request. It is not supposed to be used by another thread with a different lifecycle. That means, that at any (random) point in time, when the request is done and the corresponding session is closed, your download thread looses its resource resolver.

That means, that your download thread needs to have a resource resolver in its own. You can use the methods of the SlingRepository service to get one, for example

ResourceResolver = slingRepository.loginService("docuSignDownloadService",null);

(You should use a restricted user with limited permissions to do it, please avoid using an admin session.)

The API Doc:

SlingRepository (Apache Sling 9 API)

A good resource for working with Service Users is Service Users in AEM

Jörg

Avatar

Level 3

ok, I see the issue with the new Thread!.  I wouldn't recommend trying to inject any object into the new runnable, maybe it's best to simply create a new connection to the repository based on a System User. Do you know how to create a "System User", map the user to a service and finally have a ResourceResolver connect to the repository using that "System User"? if not I can gladly send instructions

Avatar

Correct answer by
Level 3

Sorry for the delay, but I finally got it working. A few notes:

My initial design was never going to work. I tried to pass the servlet's request.getResourceResolver() down to my worker class and then reference it in the new Thread. But when the servlet then issues a redirect (or any other response) back to the client, the request, and therefore the ResourceResolver, is closed, and the thread throws an exception when trying to use the now-closed resolver.

My second attempt was close, but I had @Reference in the wrong place. Without an @Service annotation, my Runnable couldn't get wired up by the SCR machinery. However, when I moved the @Reference to be a class member of the worker class containing the method that spawns the thread, I'm able to use the factory to get a new resolver. So my code now looks like this:

@Component(metatype = true)

@Service

public class DocumentSigningProviderDocuSignImpl implements DocumentSigningProvider {

    @Reference

    private ResourceResolverFactory resolverFactory;

    @Override

    public void getDocFromService(Map<String, String> params) {

        new Thread( new Runnable() {

            public void run() {

                ResourceResolver resolver = resolverFactory.getAdministrativeResourceResolver(null);

                Session session = resolver.adaptTo(Session.class);  

which works as expected.

Everyone is correct in pointing out that using an AdministrativeResourceResolver is wrong; I even put a comment in my code to that effect:

// security hole, fix later

But that wasn't the problem I was trying to solve, and changing to a service account doesn't make the code work. Using an Admin resolver was just a quick-and-dirty way to get running; we (as I'm sure most AEM customers) have this throughout our codebase (I just counted 40 occurrences), and now that my code works as expected I'll go back and replace it with a Service account.

Thanks for the help and advice!