0

I am implementing an upload feature using Grails where basically a user gets to upload a text file and then the system will persist each line of that text file as a database record. While the uploading works fine, larger files take time to process and therefore they ask to have a progress bar so that users can determine if their upload is still processing or an actual error has occurred.

To do this, what I did is to create two URLs:

  • /upload which is the actual URL that receives the uploaded text file.
  • /upload/status?uploadToken= which returns the status of a certain upload based on its uploadToken.

What I did is after processing each line, the service will update a session-level counter variable:

// import ...

class UploadService {
    Map upload(CommonsMultipartFile record, GrailsParameterMap params) {
        Map response = [success: true]

        try {
            File file = new File(record.getOriginalFilename())
            FileUtils.writeByteArrayToFile(file, record.getBytes())
            HttpSession session = WebUtils.retrieveGrailsWebRequest().session
            List<String> lines = FileUtils.readLines(file, "UTF-8"), errors = []
            String uploadToken = params.uploadToken

            session.status.put(uploadToken,
                [message: "Checking content of the file of errors.",
                    size: lines.size(),
                    done: 0])

            lines.eachWithIndex { l, li ->
                // ... regex checking per line and appending any error to the errors List

                session.status.get(uploadToken).done++
            }

            if(errors.size() == 0) {
                session.status.put(uploadToken,
                    [message: "Persisting record to the database.",
                        size: lines.size(),
                        done: 0])

                lines.eachWithIndex { l, li ->
                    // ... Performs GORM manipulation here

                    session.status.get(uploadToken).done++
                }           
            }
            else {
                response.success = false
            }
        }
        catch(Exception e) {
            response.success = false
        }

        response << [errors: errors]

        return response
    }
}

Then create a simple WebSocket implementation that connects to the /upload/status?uploadToken= URL. The problem is that I cannot access the session variable on POGOs. I even change that POGO into a Grails service because I thought that is the cause of the issue, but I still can't access the session variable.

// import ...

@ServerEndpoint("/upload/status")
@WebListener
class UploadEndpointService implements ServletContextListener {    
    @OnOpen
    public void onOpen(Session userSession) { /* ... */ }

    @OnClose
    public void onClose(Session userSession, CloseReason closeReason) { /* ... */ }

    @OnError
    public void onError(Throwable t) { /* ... */ }

    @OnMessage
    public void onMessage(String token, Session userSession) {
        // Both of these cause IllegalStateException
        def session = WebUtils.retrieveGrailsWebRequest().session
        def session = RequestContextHolder.currentRequestAttributes().getSession()

        // This returns the session id but I don't know what to do with that information.
        String sessionId = userSession.getHttpSessionId() 

        // Sends the upload status through this line    
        sendMessage((session.get(token) as JSON).toString(), userSession)
    }

    private void sendMessage(String message, Session userSession = null) {
        Iterator<Session> iterator = users.iterator()
        while(iterator.hasNext()) {
            iterator.next().basicRemote.sendText(message)
        }
    }
}

And instead, gives me an error:

Caused by IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.

I already verified that the web socket is working by making it send a static String content. But what I want is to be able to get that counter and set it as the send message. I'm using Grails 2.4.4 and the Grails Spring Websocket plugin, while looks promising, is only available from Grails 3 onwards. Is there any way to achieve this, or if not, what approach should I use?

Gideon
  • 1,469
  • 2
  • 26
  • 57
  • https://stackoverflow.com/questions/43250539/grails-no-thread-bound-request-found – V H Mar 02 '18 at 17:27

1 Answers1

0

Much thanks to the answer to this question that helped me greatly solving my problem.

I just modified my UploadEndpointService the same as the one on that answer and instead of making it as a service class, I reverted it back into a POGO. I also configured it's @Serverendpoint annotation and added a configurator value. I also added a second parameter to the onOpen() method. Here is the edited class:

import grails.converters.JSON
import grails.util.Environment
import javax.servlet.annotation.WebListener
import javax.servlet.http.HttpSession
import javax.servlet.ServletContext
import javax.servlet.ServletContextEvent
import javax.servlet.ServletContextListener
import javax.websocket.CloseReason
import javax.websocket.EndpointConfig
import javax.websocket.OnClose
import javax.websocket.OnError
import javax.websocket.OnMessage
import javax.websocket.OnOpen
import javax.websocket.server.ServerContainer
import javax.websocket.server.ServerEndpoint
import javax.websocket.Session
import org.apache.log4j.Logger
import org.codehaus.groovy.grails.commons.GrailsApplication
import org.codehaus.groovy.grails.web.json.JSONObject
import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes
import org.springframework.context.ApplicationContext

@ServerEndpoint(value="/ep/maintenance/attendance-monitoring/upload/status", configurator=GetHttpSessionConfigurator.class)
@WebListener
class UploadEndpoint implements ServletContextListener {
    private static final Logger log = Logger.getLogger(UploadEndpoint.class)
    private Session wsSession
    private HttpSession httpSession

    @Override
    void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.servletContext
        ServerContainer serverContainer = servletContext.getAttribute("javax.websocket.server.ServerContainer")

        try {
            if (Environment.current == Environment.DEVELOPMENT) {
                serverContainer.addEndpoint(UploadEndpoint)
            }

            ApplicationContext ctx = (ApplicationContext) servletContext.getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT)
            GrailsApplication grailsApplication = ctx.grailsApplication
            serverContainer.defaultMaxSessionIdleTimeout = grailsApplication.config.servlet.defaultMaxSessionIdleTimeout ?: 0
        } catch (IOException e) {
            log.error(e.message, e)
        }
    }

    @Override
    void contextDestroyed(ServletContextEvent servletContextEvent) {
    }

    @OnOpen
    public void onOpen(Session userSession, EndpointConfig config) {
        this.wsSession = userSession
        this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName())
    }

    @OnMessage
    public void onMessage(String message, Session userSession) {
        try {
            Map params = new JSONObject(message)
            if(httpSession.status == null) {
                params = [message: "Initializing file upload.",
                    size: 0,
                    token: 0]
                sendMessage((params as JSON).toString())
            }
            else {
                sendMessage((httpSession.status.get(params.token) as JSON).toString())
            }
        }
        catch(IllegalStateException e) {
        }
    }

    @OnClose
    public void onClose(Session userSession, CloseReason closeReason) {
        try {
            userSession.close()
        }
        catch(IllegalStateException e) {
        }
    }

    @OnError
    public void onError(Throwable t) {
        log.error(t.message, t)
    }

    private void sendMessage(String message, Session userSession=null) {
        wsSession.basicRemote.sendText(message)
    }
}

The real magic happens within the onOpen() method. There is where the accessing of the session variable takes place.

Gideon
  • 1,469
  • 2
  • 26
  • 57