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?

1

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.