aboutsummaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@vespa.ai>2023-10-20 15:49:28 +0200
committerBjørn Christian Seime <bjorncs@vespa.ai>2023-10-20 16:18:28 +0200
commit12b1d52427cc3decb98f24f24739affe4fc3fb09 (patch)
treee10abd5811d3514a3427646aa5e7fc1bc309cb3f /controller-server/src/main/java
parent15348a70192e827ee8c1df14ad22493bcae90570 (diff)
Move out mail templating to separate class
Diffstat (limited to 'controller-server/src/main/java')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java117
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java73
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java5
7 files changed, 143 insertions, 87 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index 87885bc5f21..bab86e7bfde 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -134,7 +134,7 @@ public class Controller extends AbstractComponent {
notifier = new Notifier(curator, serviceRegistry.zoneRegistry(), serviceRegistry.mailer(), flagSource);
notificationsDb = new NotificationsDb(this);
supportAccessControl = new SupportAccessControl(this);
- mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry().dashboardUrl(), tenantController, serviceRegistry.mailer(), curator, clock);
+ mailVerifier = new MailVerifier(serviceRegistry.zoneRegistry(), tenantController, serviceRegistry.mailer(), curator, clock);
dataplaneTokenService = new DataplaneTokenService(this);
// Record the version of this controller
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
index ecdfc5990c0..7d8f0d260fc 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
@@ -6,22 +6,21 @@ import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.TenantController;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
+import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import java.net.URI;
import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import static com.yahoo.yolean.Exceptions.uncheck;
-
/**
* @author olaa
@@ -34,14 +33,14 @@ public class MailVerifier {
private final Mailer mailer;
private final CuratorDb curatorDb;
private final Clock clock;
- private final URI dashboardUri;
+ private final MailTemplating mailTemplating;
- public MailVerifier(URI dashboardUri, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) {
+ public MailVerifier(ZoneRegistry zoneRegistry, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) {
this.tenantController = tenantController;
this.mailer = mailer;
this.curatorDb = curatorDb;
this.clock = clock;
- this.dashboardUri = dashboardUri;
+ this.mailTemplating = new MailTemplating(zoneRegistry);
}
public PendingMailVerification sendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) {
@@ -133,12 +132,7 @@ public class MailVerifier {
}
private Mail mailOf(PendingMailVerification pendingMailVerification) {
- var classLoader = this.getClass().getClassLoader();
- var template = uncheck(() -> classLoader.getResourceAsStream("mail/mail-verification.tmpl").readAllBytes());
- var message = new String(template)
- .replaceAll("%\\{consoleUrl}", dashboardUri.getHost())
- .replaceAll("%\\{email}", pendingMailVerification.getMailAddress())
- .replaceAll("%\\{code}", pendingMailVerification.getVerificationCode());
+ var message = mailTemplating.generateMailVerificationHtml(pendingMailVerification);
return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
index ba2e3099d5d..d0426416349 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.flags.ListFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
+import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications;
@@ -160,7 +161,7 @@ public class CloudTrialExpirer extends ControllerMaintainer {
}
private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, String emailMsg) {
- var mail = Optional.of(Notification.MailContent.fromTemplate("default-mail-content")
+ var mail = Optional.of(Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT)
.subject(emailSubject)
.with("mailMessageTemplate", "cloud-trial-notification")
.with("cloudTrialMessage", emailMsg)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java
new file mode 100644
index 00000000000..e8fb7289f4c
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java
@@ -0,0 +1,117 @@
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+package com.yahoo.vespa.hosted.controller.notification;
+
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.restapi.UriBuilder;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
+import com.yahoo.yolean.Exceptions;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.Velocity;
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
+import org.apache.velocity.runtime.resource.util.StringResourceRepository;
+import org.apache.velocity.tools.generic.EscapeTool;
+
+import java.io.StringWriter;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * @author bjorncs
+ */
+public class MailTemplating {
+
+ public enum Template {
+ MAIL("mail"), DEFAULT_MAIL_CONTENT("default-mail-content"), NOTIFICATION_MESSAGE("notification-message"),
+ CLOUD_TRIAL_NOTIFICATION("cloud-trial-notification"), MAIL_VERIFICATION("mail-verification");
+
+ public static Optional<Template> fromId(String id) {
+ return Arrays.stream(values()).filter(t -> t.id.equals(id)).findAny();
+ }
+
+ private final String id;
+
+ Template(String id) { this.id = id; }
+
+ public String getId() { return id; }
+ }
+
+ private final VelocityEngine velocity;
+ private final EscapeTool escapeTool = new EscapeTool();
+ private final URI dashboardUri;
+
+ public MailTemplating(ZoneRegistry zoneRegistry) {
+ this.velocity = createTemplateEngine();
+ this.dashboardUri = zoneRegistry.dashboardUrl();
+ }
+
+ public String generateDefaultMailHtml(Template mailBodyTemplate, Map<String, Object> params, TenantName tenant) {
+ var ctx = createVelocityContext();
+ ctx.put("accountNotificationLink", accountNotificationsUri(tenant));
+ ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html");
+ ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html"));
+ ctx.put("supportLink", consoleUri("support"));
+ ctx.put("mailBodyTemplate", mailBodyTemplate.getId());
+ params.forEach(ctx::put);
+ return render(ctx, Template.MAIL);
+ }
+
+ public String generateMailVerificationHtml(PendingMailVerification pmf) {
+ var ctx = createVelocityContext();
+ ctx.put("consoleLink", dashboardUri.getHost());
+ ctx.put("email", pmf.getMailAddress());
+ ctx.put("code", pmf.getVerificationCode());
+ return render(ctx, Template.MAIL_VERIFICATION);
+ }
+
+ public String escapeHtml(String s) { return escapeTool.html(s); }
+
+ private VelocityContext createVelocityContext() {
+ var ctx = new VelocityContext();
+ ctx.put("esc", escapeTool);
+ return ctx;
+ }
+
+ private String render(VelocityContext ctx, Template template) {
+ var writer = new StringWriter();
+ // Ignoring return value - implementation either returns 'true' or throws, never 'false'
+ velocity.mergeTemplate(template.getId(), StandardCharsets.UTF_8.name(), ctx, writer);
+ return writer.toString();
+ }
+
+ private static VelocityEngine createTemplateEngine() {
+ var v = new VelocityEngine();
+ v.setProperty(Velocity.RESOURCE_LOADERS, "string");
+ v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName());
+ v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false");
+ v.init();
+ var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
+ Arrays.stream(Template.values()).forEach(t -> registerTemplate(repo, t.getId()));
+ return v;
+ }
+
+ private static void registerTemplate(StringResourceRepository repo, String name) {
+ var templateStr = Exceptions.uncheck(() -> {
+ var in = MailTemplating.class.getResourceAsStream("/mail/%s.vm".formatted(name));
+ return new String(in.readAllBytes());
+ });
+ repo.putStringResource(name, templateStr);
+ }
+
+ private String accountNotificationsUri(TenantName tenant) {
+ return new UriBuilder(dashboardUri)
+ .append("tenant/")
+ .append(tenant.value())
+ .append("account/notifications")
+ .toString();
+ }
+
+ private String consoleUri(String path) {
+ return new UriBuilder(dashboardUri).append(path).toString();
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
index 525a768457f..4a94098ce98 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
@@ -90,7 +90,7 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve
}
public static class MailContent {
- private final String template;
+ private final MailTemplating.Template template;
private final SortedMap<String, Object> values;
private final String subject;
@@ -100,18 +100,18 @@ public record Notification(Instant at, Notification.Type type, Notification.Leve
subject = b.subject;
}
- public String template() { return template; }
+ public MailTemplating.Template template() { return template; }
public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); }
public Optional<String> subject() { return Optional.ofNullable(subject); }
- public static Builder fromTemplate(String template) { return new Builder(template); }
+ public static Builder fromTemplate(MailTemplating.Template template) { return new Builder(template); }
public static class Builder {
- private final String template;
+ private final MailTemplating.Template template;
private final Map<String, Object> values = new HashMap<>();
private String subject;
- private Builder(String template) {
+ private Builder(MailTemplating.Template template) {
this.template = template;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
index 6468a4c397b..e3bfb8b4c56 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.notification;
import com.google.common.annotations.VisibleForTesting;
import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.TenantName;
import com.yahoo.restapi.UriBuilder;
import com.yahoo.text.Text;
import com.yahoo.vespa.flags.FetchVector;
@@ -16,17 +15,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.yolean.Exceptions;
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.app.Velocity;
-import org.apache.velocity.app.VelocityEngine;
-import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
-import org.apache.velocity.runtime.resource.util.StringResourceRepository;
-import org.apache.velocity.tools.generic.EscapeTool;
-
-import java.io.StringWriter;
+
import java.net.URI;
-import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -46,7 +36,7 @@ public class Notifier {
private final FlagSource flagSource;
private final NotificationFormatter formatter;
private final URI dashboardUri;
- private final VelocityEngine velocity;
+ private final MailTemplating mailTemplating;
private static final Logger log = Logger.getLogger(Notifier.class.getName());
@@ -59,29 +49,7 @@ public class Notifier {
this.flagSource = Objects.requireNonNull(flagSource);
this.formatter = new NotificationFormatter(zoneRegistry);
this.dashboardUri = zoneRegistry.dashboardUrl();
- this.velocity = createTemplateEngine();
- }
-
- private static VelocityEngine createTemplateEngine() {
- var v = new VelocityEngine();
- v.setProperty(Velocity.RESOURCE_LOADERS, "string");
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName());
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false");
- v.init();
- var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
- registerTemplate(repo, "mail");
- registerTemplate(repo, "default-mail-content");
- registerTemplate(repo, "notification-message");
- registerTemplate(repo, "cloud-trial-notification");
- return v;
- }
-
- private static void registerTemplate(StringResourceRepository repo, String name) {
- var templateStr = Exceptions.uncheck(() -> {
- var in = Notifier.class.getResourceAsStream("/mail/%s.vm".formatted(name));
- return new String(in.readAllBytes());
- });
- repo.putStringResource(name, templateStr);
+ this.mailTemplating = new MailTemplating(zoneRegistry);
}
public void dispatch(List<Notification> notifications, NotificationSource source) {
@@ -154,26 +122,13 @@ public class Notifier {
}
private String generateHtml(FormattedNotification content) {
- var esc = new EscapeTool();
- var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content, esc));
- var ctx = new VelocityContext();
- ctx.put("esc", esc);
- ctx.put("accountNotificationLink", accountNotificationsUri(content.notification().source().tenant()));
- ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html");
- ctx.put("termsOfServiceLink", consoleUri("terms-of-service-trial.html"));
- ctx.put("supportLink", consoleUri("support"));
- ctx.put("mailBodyTemplate", mailContent.template());
- mailContent.values().forEach(ctx::put);
-
- var writer = new StringWriter();
- // Ignoring return value - implementation either returns 'true' or throws, never 'false'
- velocity.mergeTemplate("mail", StandardCharsets.UTF_8.name(), ctx, writer);
- return writer.toString();
+ var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content));
+ return mailTemplating.generateDefaultMailHtml(mailContent.template(), mailContent.values(), content.notification().source().tenant());
}
- private Notification.MailContent generateContentFromMessages(FormattedNotification f, EscapeTool esc) {
- var items = f.notification().messages().stream().map(m -> capitalise(linkify(esc.html(m)))).toList();
- return Notification.MailContent.fromTemplate("default-mail-content")
+ private Notification.MailContent generateContentFromMessages(FormattedNotification f) {
+ var items = f.notification().messages().stream().map(m -> capitalise(linkify(mailTemplating.escapeHtml(m)))).toList();
+ return Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT)
.with("mailMessageTemplate", "notification-message")
.with("mailTitle", "Vespa Cloud Notifications")
.with("notificationHeader", f.messagePrefix())
@@ -195,18 +150,6 @@ public class Notifier {
return sb.toString();
}
- private String accountNotificationsUri(TenantName tenant) {
- return new UriBuilder(dashboardUri)
- .append("tenant/")
- .append(tenant.value())
- .append("account/notifications")
- .toString();
- }
-
- private String consoleUri(String path) {
- return new UriBuilder(dashboardUri).append(path).toString();
- }
-
private String notificationLink(NotificationSource source) {
var uri = new UriBuilder(dashboardUri);
uri = uri.append("tenant").append(source.tenant().value());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
index 52766f699ac..d5be4d22dc2 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
@@ -12,6 +12,7 @@ import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
@@ -66,7 +67,7 @@ public class NotificationsSerializer {
notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber));
notification.mailContent().ifPresent(mc -> {
- notificationObject.setString("mail-template", mc.template());
+ notificationObject.setString("mail-template", mc.template().getId());
mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s));
var mailParamsCursor = notificationObject.setObject("mail-params");
mc.values().forEach((key, value) -> {
@@ -119,7 +120,7 @@ public class NotificationsSerializer {
private Optional<Notification.MailContent> mailContentFrom(final Inspector inspector) {
return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> {
- var builder = Notification.MailContent.fromTemplate(template);
+ var builder = Notification.MailContent.fromTemplate(MailTemplating.Template.fromId(template).orElseThrow());
SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject);
inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> {
switch (insp.type()) {