Search code examples
javaplayframeworkplayframework-2.2

Render scala template outside of controller? (Play 2)


I'm trying to decouple the sending of notification emails from the events that cause them. So far I'm passing a mail object (DocumentIssuedMail) from a controller to an Akka actor(EmailDispatcher), which is then sending the mail via the play-easymail wrapper of the Play mailer plugin. The email body is generated by the mail object after being passed to the actor, and the HTML is generated from a Scala template.

This template contains link with absolute URLs, obtained by calling

@routes.SomeController.someAction().absoluteURL()

However, I'm getting a RuntimeException when trying to render the template.

The stack trace is as follows:

java.lang.RuntimeException: There is no HTTP Context available from here.
    at play.mvc.Http$Context.current(Http.java:30)
    at play.mvc.Http$Context$Implicit.ctx(Http.java:196)
    at play.core.j.PlayMagicForJava$.requestHeader(TemplateMagicForJava.scala:56)
    at views.html.email._learner_main$.apply(_learner_main.template.scala:41)
    at views.html.documents.email.new_doc_unregistered$.apply(new_doc_unregistered.template.scala:47)
    at views.html.documents.email.new_doc_unregistered$.render(new_doc_unregistered.template.scala:67)
    at views.html.documents.email.new_doc_unregistered.render(new_doc_unregistered.template.scala)
    at email.DocumentIssuedMail.getUnregisteredMail(DocumentIssuedMail.java:71)
    at email.DocumentIssuedMail.getMail(DocumentIssuedMail.java:67)
    at actors.email.EmailDispatcher.onReceive(EmailDispatcher.java:32)
    at akka.actor.UntypedActor$$anonfun$receive$1.applyOrElse(UntypedActor.scala:167)
    at akka.actor.ActorCell.receiveMessage(ActorCell.scala:498)
    at akka.actor.ActorCell.invoke(ActorCell.scala:456)
    at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:237)
    at akka.dispatch.Mailbox.run(Mailbox.scala:219)
    at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(AbstractDispatcher.scala:386)
    at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
    at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
    at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
    at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)

Is it possible to render the template in that location, or do I need to do it on the original thread?


Solution

  • One possible solution for this problem is to pass a http request explicitly to your actor and then to a mail template.

    In a template you pass this request to a absoluteURL() method:

    @(requestHeader: play.api.mvc.RequestHeader)
    
    @main("") {
        @routes.Application.someAction().absoluteURL()(requestHeader)
    }
    

    Along with DocumentIssuedMail you need to pass a request to an actor. Here is a simple DTO.

    import play.api.mvc.RequestHeader;
    
    public class DocumentIssuedMailWrapper {
        private DocumentIssuedMail documentIssuedMail;
        private RequestHeader requestHeader;
    
        public DocumentIssuedMailWrapper(DocumentIssuedMail documentIssuedMail, RequestHeader requestHeader) {
            this.documentIssuedMail = documentIssuedMail;
            this.requestHeader = requestHeader;
        }
    
        public DocumentIssuedMail getDocumentIssuedMail() {
            return documentIssuedMail;
        }
    
        public RequestHeader getRequestHeader() {
            return requestHeader;
        }
    }
    

    The actor passes a request from the DTO to the mail template as an ordinary parameter.

    import akka.actor.UntypedActor;
    import play.api.templates.Html;
    import views.html.mail;
    
    public class EmailDispatcher extends UntypedActor {
    
        @Override
        public void onReceive(Object message) throws Exception {
            if (message instanceof DocumentIssuedMailWrapper) {
                DocumentIssuedMailWrapper wrapper = (DocumentIssuedMailWrapper) message;
                Html mailTemplate = mail.render(wrapper.getRequestHeader());
                //sending mail
            }
        }
    
    }
    

    In a controller you are able to get a request by calling ctx()._requestHeader() method. Now you just need to schedule a job with the actor and pass the request with the DTO.

    import akka.actor.ActorRef;
    import akka.actor.Props;
    import play.libs.Akka;
    import play.mvc.*;
    
    import scala.concurrent.duration.Duration;
    
    import java.util.concurrent.TimeUnit;
    
    public class Application extends Controller {
    
        public static Result sendMail() {
            DocumentIssuedMailWrapper wrapper = new DocumentIssuedMailWrapper(new DocumentIssuedMail(), ctx()._requestHeader());
    
            ActorRef emailDispatcher = Akka.system().actorOf(Props.create(EmailDispatcher.class));
            Akka.system().scheduler().scheduleOnce(Duration.create(0, TimeUnit.MILLISECONDS), emailDispatcher, wrapper, Akka.system().dispatcher(), null);
            return ok("Mail sent");
        }
    
        public static Result someAction() {
            return ok("Some other action");
        }
    
    }