From ba3e58c04e918723233dcc66996399eeeff24007 Mon Sep 17 00:00:00 2001 From: Matthias Andreas Benkard Date: Sun, 1 Nov 2020 12:58:35 +0100 Subject: KB68 Implement newsletter sending. Change-Id: I1d56e40d7f35d6be77fde1a1e8519a91bd2dc3b8 --- .../mulk/mulkcms2/benki/newsletter/Newsletter.java | 29 +++++ .../benki/newsletter/NewsletterSender.java | 117 +++++++++++++++++++++ .../benki/newsletter/NewsletterSubscription.java | 29 +++++ .../java/eu/mulk/mulkcms2/benki/posts/Post.java | 20 +++- 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java create mode 100644 src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java (limited to 'src/main/java') diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java new file mode 100644 index 0000000..3d9a3fe --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/Newsletter.java @@ -0,0 +1,29 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import eu.mulk.mulkcms2.benki.posts.Post; +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import java.time.OffsetDateTime; +import java.util.Collection; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +@Entity +@Table(name = "newsletters", schema = "benki") +public class Newsletter extends PanacheEntityBase { + + @Id + @Column(name = "id", nullable = false) + public Integer id; + + @Column(name = "date", nullable = false) + public OffsetDateTime date = OffsetDateTime.now(); + + @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY) + @OrderBy("date") + public Collection> posts; +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java new file mode 100644 index 0000000..1f13c08 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSender.java @@ -0,0 +1,117 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import static java.util.stream.Collectors.partitioningBy; +import static java.util.stream.Collectors.toList; + +import eu.mulk.mulkcms2.benki.bookmarks.Bookmark; +import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; +import eu.mulk.mulkcms2.benki.posts.Post; +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.panache.common.Sort; +import io.quarkus.qute.TemplateExtension; +import io.quarkus.qute.api.CheckedTemplate; +import io.quarkus.scheduler.Scheduled; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import javax.annotation.CheckForNull; +import javax.enterprise.context.Dependent; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Transactional; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.hibernate.Session; + +@Dependent +public class NewsletterSender { + + private static final DateTimeFormatter humanDateFormatter = + DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); + + @ConfigProperty(name = "mulkcms.newsletter.time-zone") + ZoneId newsletterTimeZone; + + @PersistenceContext EntityManager em; + + @CheckedTemplate + static class Templates { + public static native MailTemplateInstance newsletter( + Newsletter newsletter, List bookmarks, List lazychatMessages); + } + + @Scheduled(cron = "0 0 0 ? * Mon") + @Transactional + void run() throws InterruptedException, TimeoutException, ExecutionException { + var session = em.unwrap(Session.class); + + List> posts = Post.list("newsletter IS NULL", Sort.ascending("date")); + Post.fetchTexts(posts); + + if (posts.isEmpty()) { + return; + } + + var postsByClass = posts.stream().collect(partitioningBy(Post::isBookmark)); + var bookmarks = + postsByClass.getOrDefault(Boolean.TRUE, List.of()).stream() + .map(x -> (Bookmark) x) + .collect(toList()); + var lazychatMessages = + postsByClass.getOrDefault(Boolean.FALSE, List.of()).stream() + .map(x -> (LazychatMessage) x) + .collect(toList()); + + var date = OffsetDateTime.now(newsletterTimeZone); + var newsletterNumber = + (int) + session + .createQuery("SELECT max(id) FROM Newsletter", Integer.class) + .uniqueResultOptional() + .map(x -> x + 1) + .orElse(1); + + var newsletter = new Newsletter(); + newsletter.id = newsletterNumber; + newsletter.date = date; + newsletter.persist(); + + posts.forEach(post -> post.newsletter = newsletter); + + var subscriberEmails = + NewsletterSubscription.streamAll() + .map(x -> x.email) + .toArray(String[]::new); + + var mailText = Templates.newsletter(newsletter, bookmarks, lazychatMessages); + var sendJob = + mailText + .subject(String.format("MulkCMS newsletter #%d", newsletterNumber)) + .bcc(subscriberEmails) + .send(); + sendJob.toCompletableFuture().get(10000, TimeUnit.SECONDS); + } + + @TemplateExtension + @CheckForNull + static String humanDate(@CheckForNull LocalDate x) { + if (x == null) { + return null; + } + return humanDateFormatter.format(x); + } + + @TemplateExtension + @CheckForNull + static String humanDate(@CheckForNull OffsetDateTime x) { + if (x == null) { + return null; + } + return humanDateFormatter.format(x); + } +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java new file mode 100644 index 0000000..7aeda60 --- /dev/null +++ b/src/main/java/eu/mulk/mulkcms2/benki/newsletter/NewsletterSubscription.java @@ -0,0 +1,29 @@ +package eu.mulk.mulkcms2.benki.newsletter; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import java.time.OffsetDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.Email; +import org.hibernate.annotations.NaturalId; + +@Entity +@Table(name = "newsletter_subscriptions", schema = "benki") +public class NewsletterSubscription extends PanacheEntityBase { + + @Id + @Column(name = "id", nullable = false) + @GeneratedValue + public Integer id; + + @Column(name = "start_date", nullable = false) + public OffsetDateTime startDate = OffsetDateTime.now(); + + @NaturalId + @Column(name = "email", nullable = false) + @Email + public String email; +} diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java index fd023d7..346b71f 100644 --- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java +++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java @@ -5,12 +5,14 @@ import static java.util.stream.Collectors.toList; import eu.mulk.mulkcms2.benki.accesscontrol.Role; import eu.mulk.mulkcms2.benki.bookmarks.Bookmark; import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage; +import eu.mulk.mulkcms2.benki.newsletter.Newsletter; import eu.mulk.mulkcms2.benki.users.User; import eu.mulk.mulkcms2.benki.users.User_; import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -69,6 +71,12 @@ public abstract class Post> extends PanacheEntityBase { @CheckForNull public OffsetDateTime date; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "newsletter", referencedColumnName = "id", nullable = true) + @CheckForNull + @JsonbTransient + public Newsletter newsletter; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "owner", referencedColumnName = "id") @CheckForNull @@ -340,18 +348,22 @@ public abstract class Post> extends PanacheEntityBase { } // Fetch texts (to avoid n+1 selects). - var postIds = forwardResults.stream().map(x -> x.id).collect(toList()); + fetchTexts(forwardResults); + + return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults); + } + + public static > void fetchTexts(Collection posts) { + var postIds = posts.stream().map(x -> x.id).collect(toList()); if (!postIds.isEmpty()) { find("SELECT p FROM Post p LEFT JOIN FETCH p.texts WHERE p.id IN (?1)", postIds).stream() .count(); } - - return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults); } @CheckForNull - protected Text getText() { + public Text getText() { var texts = getTexts(); if (texts.isEmpty()) { return null; -- cgit v1.2.3