How to send an HTML email from a scheduler with Play 2.5?


(Cyril Franceschini) #1

Hi,

I have a scheduler to query periodically users inactivity and send an e-mail to inform them when their account will be disabled.

public class UserChecker
{
    private static final FiniteDuration INTERVAL = Duration.create(1, TimeUnit.DAYS);

    private ActorSystem actorSystem;
    private ExecutionContext executionContext;

    @Inject
    MailerClient mailerClient;

    @Inject
    Configuration configuration;

    @Inject
    public UserChecker(ActorSystem actorSystem, ExecutionContext executionContext)
    {
        this.actorSystem = actorSystem;
        this.executionContext = executionContext;

        this.initialize();
    }

    private void initialize()
    {
        actorSystem.scheduler().schedule(
            Duration.create(0, TimeUnit.SECONDS),
            INTERVAL,
            () -> check(),
            executionContext
        );
    }

    private void check()
    {        
        for (User user : User.findOutdatedPrepay())
        {
            Email email = new Email();
            email.setSubject("Subject");
            email.setFrom(Format.pretty(configuration.getString("coordinates.name"), configuration.getString("play.mailer.user")));
            email.addTo("test@me.com");
            email.setBodyHtml(prepayNotRefundedWarning.render(user, configuration).body());

            try
            {
                    mailerClient.send(email);
            }
            catch (Exception e)
            {
                    e.printStackTrace();
            }
        }
    }
}

I would like to use a view to render the HTML of my email but I get an error at the line where the view is rendered:

java.lang.RuntimeException: There is no HTTP Context available from here.

How should I do that?
Regards


(Marcos Pereira) #2

Hi @cyrilfr,

At some point, that the stack trace will reveal, you are trying to access the current Http.Context. From a scheduler point of view, that won’t make sense since there is no current request available to the scheduler. It means the scheduler needs to have all the necessary data to run without relying on any request data.

Since I’m not seeing any access to Http.Context in the code you shared, it is maybe happening in prepayNotRefundedWarning view? Better if you can share the complete stack trace.

Best.


(Cyril Franceschini) #3

Hi @marcospereira,

Yes, there is reverse routing in the view.

Now I get another error: java.util.NoSuchElementException: None.get


(Marcos Pereira) #4

@cyrilfr,

Yes, better to not use that since it depends on the request (to compose the absolute URL with scheme, host, etc.). Better to have this as a configuration that you can access and use the reverse router just to get the path section.

Now I get another error: java.util.NoSuchElementException: None.get

Hard to know what is happening based only on this line. Post the complete stack trace. But usually, you don’t do a get on an Option because it can be None. Better to use getOrElse if this is inside a view.

Best.


(Cyril Franceschini) #5

Here is the view:

@(user: User, configuration: play.Configuration)
@import helpers.Format

@amount = {
    font-size: 30px;
    font-weight: 300;
}

@style = {
    .amount { @amount }
}

@layouts.email(configuration, true, style) {
    <p>@Messages("prepay.head")</p>
    <table border="0" cellpadding="5" width="100%">
        <tr>
            <td align="right" valign="top" width="50%"><strong>@Messages("form.user") :</strong></td>
            <td align="left" valign="top">@user.getLabel()</td>
        </tr>
        <tr>
            <td align="right" valign="top" width="50%"><strong>@Messages("form.user.building") :</strong></td>
            <td align="left" valign="top">@user.getAddress1()<br>@user.getPostalCode() @user.getCity()</td>
        </tr>
        @if(user.getEmail() != null && !user.getEmail().isEmpty()) {
            <tr>
                <td align="right" valign="top" width="50%"><strong>@Messages("form.user.email") :</strong></td>
                <td align="left" valign="top">@user.getEmail()</td>
            </tr>
        }
        @if(user.getMobile() != null && !user.getMobile().isEmpty()) {
            <tr>
                <td align="right" valign="top"><strong>@Messages("form.user.mobile") :</strong></td>
                <td align="left" valign="top">@user.getMobile()</td>
            </tr>
        }
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.lastRefill"):</strong></td>
            <td align="left" valign="top">@Format.date(Messages("format.dateTime"), user.getLastRefill())</td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.credit") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(user.getCredit())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.prepay") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount())</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.fees"):</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount">@Messages("currency.swissFranc.short") @Format.price(configuration.getInt("eeproperty.tax.prepayReminder"))</div></td>
        </tr>
        <tr>
            <td align="right" valign="top"><strong>@Messages("form.user.total") :</strong></td>
            <td align="left" valign="top"><div style="@amount" class="amount"><u>@Messages("currency.swissFranc.short") @Format.price(user.getPrepayAmount() + configuration.getInt("eeproperty.tax.prepayReminder"))</u></div></td>
        </tr>
    </table>
}

The None.get error message was related to a bad Configuration dependency injection. Now I have troubles with Messages: java.lang.IllegalArgumentException: Unknown pattern letter: f which mean that the “format.dateTime” label isn’t matched in the language file.
This label exists and works perfecly when the view is rendered in a controller.

My Format.date() method looks like:

public static String date(String format, LocalDateTime date)
{
    if (format == null || date == null) return "";
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
    return date.atZone(ZoneId.systemDefault()).format(formatter);
}

(Marcos Pereira) #6

Hey @cyrilfr,

It is very hard to know what could be possibly happening when all we got is just a small section of the stack trace. Can you better detail what you are trying to do? How your project is configured? Where is this error happening? What is the complete stack trace?

Best.


(Cyril Franceschini) #7

What is the complete stack trace?

@marcospereira,

The complete stacktrace is:

java.lang.IllegalArgumentException: Unknown pattern letter: f
	at java.time.format.DateTimeFormatterBuilder.parsePattern(DateTimeFormatterBuilder.java:1661)
	at java.time.format.DateTimeFormatterBuilder.appendPattern(DateTimeFormatterBuilder.java:1570)
	at java.time.format.DateTimeFormatter.ofPattern(DateTimeFormatter.java:536)
	at helpers.Format.date(Format.java:37)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.apply(prepayNotRefundedWarning.template.scala:83)
	at views.html.mail.prepayNotRefundedWarning_Scope0$prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala:99)
	at views.html.mail.prepayNotRefundedWarning.render(prepayNotRefundedWarning.template.scala)
	at actors.UserChecker.check(UserChecker.java:82)
	at actors.UserChecker.lambda$initialize$0(UserChecker.java:57)
	at akka.actor.LightArrayRevolverScheduler$$anon$2$$anon$1.run(LightArrayRevolverScheduler.scala:102)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:39)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(AbstractDispatcher.scala:415)
	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)

This line: Format.date(Messages("format.dateTime", user.getLastRefill()) should be displaying the date in the right format, depending on the language file. For example, in german it would be Format.date("dd.mm.YYYY HH:mm", user.getLastRefill()) and display 27.11.2017 13:30.
Instead it gets Format.date("format.dateTime", user.getLastRefill()) and f isn’t a valid DateTimeFormatter pattern.

I guess Messages() is doing something behind the scene to work with a controller action that is missing here.

Can you better detail what you are trying to do? How your project is configured? Where is this error happening?

I want to send e-mail automatically to specific users. The error happen when the e-mail is being sent.


(Marcos Pereira) #8

And which format are you using? I mean, what is the value for format in Format.date? It looks like you are using an invalid format since lowercased f is not a defined for format patterns:

https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html

Best.


(Cyril Franceschini) #9

@marcospereira

The format normally returned is dd.MM.yyyy HH:mm:ss. In this case, the f is because Messages() doesn’t work.


(Cyril Franceschini) #10

@marcospereira

I guess that because the Http.Context is not available, Messages() cannot know what language to use for the translation. Is there a way to pass the language code to use (for ex the language of the user)?