0
votes

Use Case: We are developing an AEM Closed User Group site where users will need to submit forms which trigger workflows. Since the users are authenticated, part of the workflow payload needs to include the user who initiated the form.

I'm considering using AEM Forms for this, which saves to nodes under /content/usergenerated/content/forms/af/my-site but the user is not mentioned in the payload (only the service user). In this case, there are two service users: workflow-service running the workflow, and fd-service which handled the form processing and initial saving. E.G. the following code called from the workflow step reports 'fd-service'

workItem.getWorkflowData().getMetaDataMap().get("userId", String.class);

To work around this constraint,

Workflow initiated from publish AEM instance: All workflow instances are created using a service user when adaptive forms, interactive communications, or letters are submitted from AEM publish instance. In these cases, the user name of the logged-in user is not captured in the workflow instance data.

I am adding a filter servlet to intercept the initial form submission before the AEM Forms servlet using a request wrapper to modify the request body adding the original userID.

In terms of forms, workflows and launchers.. This is basically the setup I have https://helpx.adobe.com/aem-forms/6/aem-workflows-submit-process-form.html

I have reviewed the following resources:

Here is the code for my wrapper

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletInputStream;
import java.io.*;

public class FormSubmitRequestWrapper extends SlingHttpServletRequestWrapper {
String requestPayload;
private static final Logger log = LoggerFactory.getLogger(FormSubmitRequestWrapper.class);

public FormSubmitRequestWrapper(SlingHttpServletRequest slingRequest) {
    super(slingRequest);

    // read the original payload into the requestPayload variable
    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = null;
    try {
        // read the payload into the StringBuilder
        InputStream inputStream = slingRequest.getInputStream();
        if (inputStream != null) {
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
            char[] charBuffer = new char[128];
            int bytesRead = -1;
            while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } else {
            // make an empty string since there is no payload
            stringBuilder.append("");
        }
    } catch (IOException ex) {
        log.error("Error reading the request payload", ex);
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException iox) {
                log.error("Error closing bufferedReader", iox);
            }
        }
    }
    requestPayload = stringBuilder.toString();
}

/**
 * Override of the getInputStream() method which returns an InputStream that reads from the
 * stored requestPayload string instead of from the request's actual InputStream.
 */
@Override
public ServletInputStream getInputStream ()
        throws IOException {

    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestPayload.getBytes());
    ServletInputStream inputStream = new ServletInputStream() {
        public int read ()
                throws IOException {
            return byteArrayInputStream.read();
        }
    };
    return inputStream;
}
}

Here is my filter

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.engine.EngineConstants;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import javax.jcr.Session;
import javax.servlet.*;
import java.io.IOException;


@Component(service = Filter.class,
        immediate = true,

        property = {
                Constants.SERVICE_DESCRIPTION + "=Add the CUG userID to any UGC posts",
                EngineConstants.SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
                Constants.SERVICE_RANKING + ":Integer=3000",
                EngineConstants.SLING_FILTER_PATTERN + "=/content/forms/af/my-site.*"
        })


public class DecorateUserGeneratedFilter implements Filter {

    private static final Logger log = LoggerFactory.getLogger(DecorateUserGeneratedFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final SlingHttpServletResponse slingResponse = (SlingHttpServletResponse ) response;
        final SlingHttpServletRequest slingRequest= (SlingHttpServletRequest) request;

        FormSubmitRequestWrapper wrappedRequest = new FormSubmitRequestWrapper(slingRequest);

        log.info("starting ConfirmAlumniStatus workflow");
        log.info(getCurrentUserId(slingRequest));

        chain.doFilter(wrappedRequest, slingResponse);
    }

    @Override
    public void destroy() {

    }

    public String getCurrentUserId(SlingHttpServletRequest request) {
        ResourceResolver resolver = request.getResourceResolver();
        Session session = resolver.adaptTo(Session.class);
        String userId = session.getUserID();

        return userId;

    }

}

When POST submissions get processed by this filter, I'm getting the error below stating the request body has already been read. So it seems the filter ranking might not be high enough.

25.06.2018 13:11:13.200 ERROR [0:0:0:0:0:0:0:1 [1529946669719] POST /content/forms/af/my-site/request-access/jcr:content/guideContainer.af.internalsubmit.jsp HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl service: Uncaught Throwable java.lang.IllegalStateException: Request Data has already been read at org.apache.sling.engine.impl.request.RequestData.getInputStream(RequestData.java:669) at org.apache.sling.engine.impl.SlingHttpServletRequestImpl.getInputStream(SlingHttpServletRequestImpl.java:292) at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:136) at my.site.servlets.FormSubmitRequestWrapper.(FormSubmitRequestWrapper.java:26) at my.site.servlets.DecorateUserGeneratedFilter.doFilter(DecorateUserGeneratedFilter.java:75) at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:68) at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:73) at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:73) at com.cognifide.cq.includefilter.DynamicIncludeFilter.doFilter(DynamicIncludeFilter.java:82) at org.apache.sling.engine.impl.filter.AbstractSlingFilterChain.doFilter(AbstractSlingFilterChain.java:68) at org.apache.sling.engine.impl.debug.RequestProgressTrackerLogFilter.doFilter(RequestProgressTrackerLogFilter.java:10

I don't think the service ranking is working. When I view http://localhost:4502/system/console/status-slingfilter my filter is listed as shown. Judging from the other filters listed, I think the leftmost number is the filter ranking. For some reason my filter is ranked 0 even though I set is as service.ranking=700

0 : class my.site.servlets.DecorateUserGeneratedFilter (id: 8402, property: service.ranking=700); called: 0; time: 0ms; time/call: -1µs

Update: I was able to fix the filter rank, making it 700 still gave the IllegalStateException. Making it 3000 made that problem go away. But when request.getInputStream() is called from my wrapper. It returns null.

3
getInputStream was probably called before your filter. I’d suggest saving the userID to a request attribute then retrieving and returning it in you wrapper. Hope this makes sense. Also, Does the userId have to be in the request body to begin with?Ahmed Musallam
I think your right about getInputStream already being called. I would usually agree that using an attribute would be a good option, but in this case I don't control the servlet. So I figure if we add it to the request body, then maybe the servlet would process our data along with the other form data submitted.Cris Rockwell
I see, in that case, it seems like the userId is not really documented? and can change in future impl. I'd recommend going a different route with a servlet that triggers your workflows. (I'll post an answer shortly)Ahmed Musallam
Correct. It's rather an unexpected issue as I expected the userid to already part of the form payload or metadata. In our case the user is authenticated and the userid is known via the method I included in the filter getCurrentUserId. It seems there should be a way to persist that for use by the workflows.Cris Rockwell
I think that forms should support triggering workflows natively. making a launcher to trigger workflow when nodes change by form submission seems like a round-about, more complicated way than it needs to be. You can simply create a servlet to process forms and in that servlet initiate the workflow.Ahmed Musallam

3 Answers

2
votes

What you are trying to do might be the easy route, but might not be future-proof for new AEM releases.

You need total control of how your workflow is triggered!:

  1. Your forms should have a field that contains the workflow path (and maybe other information needed for that workflow)
  2. Create a custom servlet that your forms will post to.
  3. In that servlet process all user posted values (from the form). But especially get a hold of the intended workflow path and trigger it using the workflow API.

This way you don't have to mess with launchers and the workflows are triggered by your users using their user id.

Hope this helps.

1
votes

Right idea, wrong location.

The short answer is that when you implement the SlingHttpServletRequestWrapper it provides a default handling of method calls to the original SlingHttpServletRequest if you're adding a parameter on the fly what you want to do is to make sure that the methods that are interacting with the parameters are overridden so that you can be sure yours is added. So on initialization, call the original parameter map, copy those items in a new map which includes your own values.

Then over ride any methods that would request those values

getParameter(String)
getParameterMap()
getParameterNames()
getParameterValues(String)

Don't touch the InputStream, that's already been processed to obtain any parameters that are being passed in.

Additionally, that is one of two ways you can handle this type of use case, the other option is to use the SlingPOSTProcessors as documented https://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html

which allows you to detect what is being written to the repository and modify the data to include, like your case, an additional field.

0
votes

if you are looking for code example :

      @SlingFilter(order = 1)
    public class MyFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
    return;
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
        FilterChain filterChain) throws IOException, ServletException {

    ServletRequest request = servletRequest;

    if (servletRequest instanceof SlingHttpServletRequest) {
        final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) servletRequest;
            request = new SlingHttpServletRequestWrapper(slingRequest) {
              String userId = getCurrentUserId(slingRequest);
            };

    }

    filterChain.doFilter(request, servletResponse);
}

@Override
public void destroy() {
    return;
}