From 81c479eb3a2625f3147ba5e9d3b576c158f8aad8 Mon Sep 17 00:00:00 2001 From: "Brian M. Long" Date: Sat, 3 Jun 2023 17:03:56 -0400 Subject: [PATCH] added agenda job/api/support --- .../java/com/poststats/golf/api/EventApi.java | 80 +++++- .../poststats/golf/job/EventAgendaJob.java | 236 ++++++++++++++++++ .../golf/service/EventDocumentService.java | 17 ++ .../golf/service/EventPersonService.java | 9 +- .../service/db/EventDocumentServiceDAO.java | 79 ++++++ .../service/db/EventPersonServiceDAO.java | 72 +++++- 6 files changed, 485 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/poststats/golf/job/EventAgendaJob.java create mode 100644 src/main/java/com/poststats/golf/service/EventDocumentService.java create mode 100644 src/main/java/com/poststats/golf/service/db/EventDocumentServiceDAO.java diff --git a/src/main/java/com/poststats/golf/api/EventApi.java b/src/main/java/com/poststats/golf/api/EventApi.java index f27c1ad..9a76ea2 100644 --- a/src/main/java/com/poststats/golf/api/EventApi.java +++ b/src/main/java/com/poststats/golf/api/EventApi.java @@ -1,12 +1,26 @@ package com.poststats.golf.api; +import java.io.IOException; +import java.math.BigInteger; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.brianlong.cache.CacheRetrievalException; import com.brianlong.util.FlexMap; import com.fasterxml.jackson.core.JsonProcessingException; import com.poststats.api.Constants; import com.poststats.golf.api.model.Event; +import com.poststats.golf.job.EventAgendaJob; +import com.poststats.golf.service.EventDocumentService; +import com.poststats.golf.service.EventPersonService; import com.poststats.golf.service.EventService; import com.poststats.golf.service.SeriesService; import com.poststats.transformer.impl.DaoConverter; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -14,14 +28,14 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; +import jakarta.mail.MessagingException; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response.Status; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * @author brian.long@poststats.com @@ -39,6 +53,15 @@ public class EventApi { @Inject private EventService eventService; + @Inject + private EventDocumentService eventDocumentService; + + @Inject + private EventPersonService eventPersonService; + + @Inject + private EventAgendaJob eventAgendaJob; + @Inject private SeriesService seriesService; @@ -89,4 +112,57 @@ public class EventApi { return this.converter.convertValue(row, Event.class); } + @POST + @Path("/document/{documentId}/send") + @Produces(Constants.V1_JSON) + @Operation( + summary = "Sends the specified document.", + description = "Sends the specified document off-schedule, regardless of when it is configured to send." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success"), + @ApiResponse(responseCode = "404", description = "An event or document with the specified ID could not be found") + }) + public void sendDocument(@PathParam("documentId") long documentId) throws CacheRetrievalException, SQLException, MessagingException, IOException { + FlexMap document = this.eventDocumentService.get(this.eventId, documentId); + if (document == null) + throw new WebApplicationException("Document not found", Status.NOT_FOUND); + + switch (document.getString("type")) { + case "agenda": + this.eventAgendaJob.send(document); + break; + default: + throw new WebApplicationException("Document is not an agenda", Status.UNSUPPORTED_MEDIA_TYPE); + } + } + + @POST + @Path("/document/{documentId}/sendTest/{personId}") + @Produces(Constants.V1_JSON) + @Operation( + summary = "Sends the specified document to the specified person.", + description = "Sends the specified document to only the specified person off-schedule." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success"), + @ApiResponse(responseCode = "404", description = "An event, document, or person with the specified ID could not be found") + }) + public void sendTestDocument(@PathParam("documentId") long documentId, @PathParam("personID") long personId) throws CacheRetrievalException, SQLException, MessagingException, IOException { + FlexMap document = this.eventDocumentService.get(this.eventId, documentId); + if (document == null) + throw new WebApplicationException("Document not found", Status.NOT_FOUND); + + FlexMap eperson = this.eventPersonService.get(this.eventId, personId); + Map recipientIds = Collections.singletonMap(personId, eperson.getBigInteger("epersonID")); + + switch (document.getString("type")) { + case "agenda": + this.eventAgendaJob.send(document, recipientIds); + break; + default: + throw new WebApplicationException("Document is not an agenda", Status.UNSUPPORTED_MEDIA_TYPE); + } + } + } diff --git a/src/main/java/com/poststats/golf/job/EventAgendaJob.java b/src/main/java/com/poststats/golf/job/EventAgendaJob.java new file mode 100644 index 0000000..fb8cff4 --- /dev/null +++ b/src/main/java/com/poststats/golf/job/EventAgendaJob.java @@ -0,0 +1,236 @@ +package com.poststats.golf.job; + +import com.brianlong.cache.CacheRetrievalException; +import com.brianlong.sql.DataSet; +import com.brianlong.sql.FlexPreparedStatement; +import com.brianlong.util.DateTimeFormatter; +import com.brianlong.util.FlexMap; +import com.poststats.golf.cache.EventCache; +import com.poststats.golf.sql.EventAutolist; +import com.poststats.provider.NonTransactionalProvider; +import com.poststats.provider.PostStatsProvider; +import com.poststats.provider.Statement; +import com.poststats.provider.StatementProvider; +import com.poststats.service.file.EnvironmentConfiguration; +import com.poststats.util.CompositeTexter; +import com.poststats.util.Contact; +import com.poststats.util.Emailer; +import jakarta.annotation.PostConstruct; +import jakarta.ejb.Schedule; +import jakarta.ejb.Singleton; +import jakarta.inject.Inject; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.math.BigInteger; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.apache.commons.io.IOUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.slf4j.Logger; + +@Singleton +public class EventAgendaJob { + + private final DateTimeFormatter weekdayFormatter = new DateTimeFormatter("EEEE"); + + @Inject + private Logger logger; + + @Inject + private EnvironmentConfiguration envConfig; + + private String baseGolfUrl; + private CompositeTexter texter; + + @PostConstruct + public void init() { + this.baseGolfUrl = this.envConfig.getString("golf.url") + + "/c"; + this.texter = new CompositeTexter(this.envConfig); + } + + @Schedule(hour = "21", timezone = "EDT") + protected void nightly() throws CacheRetrievalException, SQLException, MessagingException, IOException { + this.logger.trace("run()"); + + // if executed after 6p, then agendas should be for following day + LocalDateTime now = LocalDateTime.now(); // (2016, 6, 7, 22, 0); + LocalDate date = now.plusHours(6).toLocalDate(); + + this.logger.info("Sending agendas for {}", date); + + List agendas = this.getAgendas(date); + if (agendas.isEmpty()) { + this.logger.debug("No agenda items to send: {}", date); + return; + } + + for (DataSet agenda : agendas) + this.send(agenda); + } + + private List getAgendas(LocalDate date) throws SQLException { + FlexPreparedStatement fps = this.sqlSelectAgendas.buildPreparedStatement(); + try { + fps.setDate(1, date); + return fps.executeQuery().getAllRows(); + } finally { + fps.close(); + } + } + + public void send(FlexMap agenda) + throws CacheRetrievalException, SQLException, MessagingException, IOException { + Long eventID = agenda.getLong("eventID"); + Map recipientIDs = EventAutolist.getInstance().getPersonIDMap("signed", eventID.longValue()); + this.send(agenda, recipientIDs); + } + + public void send(FlexMap agenda, Map recipientIDs) + throws CacheRetrievalException, SQLException, MessagingException, IOException { + boolean doEmailLink = Boolean.TRUE.equals(agenda.getBoolean("sendEmailWithLink")); + boolean doEmail = Boolean.TRUE.equals(agenda.getBoolean("sendEmailWithContent")); + boolean doText = Boolean.TRUE.equals(agenda.getBoolean("sendTextWithLink")); + if (!doText && !doEmailLink && !doEmail) + return; + + Long eventID = agenda.getLong("eventID"); + Long documentID = agenda.getLong("documentID"); + LocalDate day = agenda.getDate("day"); + + this.logger.debug("Sending agenda with document ID: {}", documentID); + + DataSet event = EventCache.getInstance().get(eventID); + String subject = event.getString("event") + + " Agenda for " + + this.weekdayFormatter.format(day); + + Set textedPersonIDs = Collections.emptySet(); + if (doText) + textedPersonIDs = this.text(eventID, documentID, recipientIDs, subject); + + if (doEmailLink) + this.emailLink(eventID, documentID, recipientIDs, textedPersonIDs, subject); + + if (doEmail) { + this.email(eventID, documentID, recipientIDs, textedPersonIDs, subject); + } + } + + private Set text(long eventID, long documentID, Map recipientIDs, String subject) + throws SQLException, MessagingException { + this.logger.debug("Sending agenda links by text with document ID: {}", documentID); + + Map recipients = Contact.getContactMapByIds(recipientIDs.keySet(), false, true); + + String baseUrl = baseGolfUrl + + "?n=documentAgenda&eventID=" + + eventID + + "&documentID=" + + documentID + + "&epersonID="; + + Set textedPersonIDs = new HashSet(recipientIDs.size()); + + for (Entry recipient : recipients.entrySet()) { + if (logger.isDebugEnabled()) + logger.debug("Sending agenda to: " + + recipient.getKey()); + String message = baseUrl + recipientIDs.get(recipient.getKey()); + this.texter.send(Arrays.asList(recipient.getValue()), subject, message); + + textedPersonIDs.add(recipient.getKey()); + } + + return textedPersonIDs; + } + + private void emailLink(long eventID, long documentID, Map recipientIDs, + Set excludePersonIDs, String subject) throws SQLException, MessagingException { + this.logger.debug("Sending agenda links by email with document ID: {}", documentID); + + Map recipients = Contact.getContactMapByIds(recipientIDs.keySet(), true, false); + + String baseUrl = this.baseGolfUrl + + "?n=documentAgenda&eventID=" + + eventID + + "&documentID=" + + documentID + + "&epersonID="; + for (Entry recipient : recipients.entrySet()) { + if (excludePersonIDs.contains(recipient.getKey())) + continue; + if (logger.isDebugEnabled()) + logger.debug("Sending agenda to: " + + recipient.getKey()); + String message = "

" + + baseUrl + + recipientIDs.get(recipient.getKey()) + + "

"; + Emailer.getInstance(this.envConfig).send(Arrays.asList(recipient.getValue()), subject, message); + } + } + + private void email(long eventID, long documentID, Map recipientIDs, + Set excludePersonIDs, String subject) throws SQLException, MessagingException, IOException { + this.logger.debug("Sending agenda contents with document ID: {}", documentID); + + Map recipients = Contact.getContactMapByIds(recipientIDs.keySet(), true, false); + + NameValuePair[] params = new NameValuePair[] { + new BasicNameValuePair("n", "documentAgendaMinimal"), + new BasicNameValuePair("eventID", String.valueOf(eventID)), + new BasicNameValuePair("documentID", String.valueOf(documentID)) + }; + + for (Entry recipient : recipients.entrySet()) { + this.logger.debug("Sending agenda to: {}", recipient.getKey()); + + HttpUriRequest request = RequestBuilder.get(this.baseGolfUrl).addParameters(params) + .addParameter("epersonID", recipientIDs.get(recipient.getKey()).toString()).build(); + + CloseableHttpClient hclient = HttpClientBuilder.create().build(); + CloseableHttpResponse response = hclient.execute(request); + try { + if (response.getStatusLine().getStatusCode() / 100 == 2) { + String html = IOUtils.toString(response.getEntity().getContent(), "utf-8"); + Emailer.getInstance(this.envConfig).send(Arrays.asList(recipient.getValue()), subject, html); + } else { + this.logger.warn("The URL could not be loaded properly: {}", request.getURI()); + } + } finally { + response.close(); + } + } + } + + + + @Inject + @NonTransactionalProvider + @PostStatsProvider + @Statement( + sql = "SELECT ED.* " + + "FROM ~g~.EventDocument ED " + + "WHERE ED.day=? " + + " AND (ED.sendEmailWithLink IS TRUE OR ED.sendEmailWithContent IS TRUE OR ED.sendTextWithLink IS TRUE)" + ) + private StatementProvider sqlSelectAgendas; + +} diff --git a/src/main/java/com/poststats/golf/service/EventDocumentService.java b/src/main/java/com/poststats/golf/service/EventDocumentService.java new file mode 100644 index 0000000..a060b72 --- /dev/null +++ b/src/main/java/com/poststats/golf/service/EventDocumentService.java @@ -0,0 +1,17 @@ +package com.poststats.golf.service; + +import com.brianlong.util.FlexMap; +import com.poststats.service.CacheableService; + +public interface EventDocumentService extends CacheableService { + + /** + * This method retrieves meta-data about the specified event document. + * + * @param eventId A unique identifier for the event. + * @param eventId A unique identifier for the document. + * @return A map of meta-data specific to the event document. + */ + FlexMap get(long eventId, long documentId); + +} diff --git a/src/main/java/com/poststats/golf/service/EventPersonService.java b/src/main/java/com/poststats/golf/service/EventPersonService.java index 9d8392f..e22e6a4 100644 --- a/src/main/java/com/poststats/golf/service/EventPersonService.java +++ b/src/main/java/com/poststats/golf/service/EventPersonService.java @@ -1,10 +1,17 @@ package com.poststats.golf.service; import com.brianlong.util.FlexMap; +import com.poststats.service.CacheableService; + +import java.math.BigInteger; import java.util.List; import java.util.Set; -public interface EventPersonService { +public interface EventPersonService extends CacheableService { + + FlexMap get(BigInteger epersonId); + + FlexMap get(long eventId, long personId); List getPeople(long eventId); diff --git a/src/main/java/com/poststats/golf/service/db/EventDocumentServiceDAO.java b/src/main/java/com/poststats/golf/service/db/EventDocumentServiceDAO.java new file mode 100644 index 0000000..fcf7ace --- /dev/null +++ b/src/main/java/com/poststats/golf/service/db/EventDocumentServiceDAO.java @@ -0,0 +1,79 @@ +package com.poststats.golf.service.db; + +import java.sql.SQLException; +import java.util.Collection; +import java.util.Map; + +import com.brianlong.sql.DataSet; +import com.brianlong.sql.FlexPreparedStatement; +import com.brianlong.util.FlexMap; +import com.poststats.golf.provider.GolfProvider; +import com.poststats.golf.service.EventDocumentService; +import com.poststats.provider.NonTransactionalProvider; +import com.poststats.provider.Statement; +import com.poststats.provider.StatementProvider; +import com.poststats.service.db.CacheableServiceDAO; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class EventDocumentServiceDAO extends CacheableServiceDAO implements EventDocumentService { + + private final long defaultCacheExpirationInSeconds = 3600; + + @Override + public FlexMap get(long eventId, long documentId) { + FlexMap document = this.get(documentId); + if (document != null && eventId != document.getLong("eventID")) + return null; + return document; + } + + @Override + protected long getCacheExpirationInSeconds() { + return this.defaultCacheExpirationInSeconds; + } + + @Override + protected DataSet fetchOne(Long documentId) throws SQLException { + FlexPreparedStatement fps = this.sqlSelectEventDocument.buildPreparedStatement(); + try { + fps.setIntegerU(2, documentId); + return fps.executeQuery().getNextRow(); + } finally { + fps.close(); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement( + sql = "SELECT ED.* " + + "FROM ~g~.EventDocument ED " + + "WHERE ED.documentId=? " + ) + private StatementProvider sqlSelectEventDocument; + + @Override + protected Map fetchBulk(Collection documentIds) throws SQLException { + FlexPreparedStatement fps = this.sqlSelectEvents.buildPreparedStatement(documentIds); + try { + return fps.executeQuery().getAllRows("documentID", Long.class); + } finally { + fps.close(); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement( + sql = "SELECT ED.* " + + "FROM ~g~.EventDocument ED " + + "WHERE ED.documentId IN (??) " + ) + private StatementProvider sqlSelectEvents; + +} diff --git a/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java b/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java index 8740d8f..da94188 100644 --- a/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java +++ b/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java @@ -1,5 +1,14 @@ package com.poststats.golf.service.db; +import java.math.BigInteger; +import java.sql.SQLException; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.brianlong.sql.DataSet; import com.brianlong.sql.FlexPreparedStatement; import com.brianlong.util.FlexMap; import com.poststats.golf.provider.GolfProvider; @@ -8,15 +17,35 @@ import com.poststats.provider.NonTransactionalProvider; import com.poststats.provider.Statement; import com.poststats.provider.StatementProvider; import com.poststats.service.ServiceException; +import com.poststats.service.db.CacheableServiceDAO; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; -import java.sql.SQLException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; @ApplicationScoped -public class EventPersonServiceDAO implements EventPersonService { +public class EventPersonServiceDAO extends CacheableServiceDAO implements EventPersonService { + + @Override + public FlexMap get(long eventId, long personId) { + try { + FlexPreparedStatement fps = this.sqlSelectEventPersonByEventIdPersonId.buildPreparedStatement(); + try { + fps.setIntegerU(1, eventId); + fps.setIntegerU(1, personId); + return fps.executeQuery().getNextRow(); + } finally { + fps.close(); + } + } catch (SQLException se) { + throw new ServiceException(se); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement(sql = "SELECT * FROM ~g~.EventPerson WHERE eventID=? AND personID=?") + private StatementProvider sqlSelectEventPersonByEventIdPersonId; @Override public List getPeople(long eventId) { @@ -94,5 +123,38 @@ public class EventPersonServiceDAO implements EventPersonService { fps.close(); } } + + @Override + protected DataSet fetchOne(BigInteger epersonId) throws SQLException { + FlexPreparedStatement fps = this.sqlSelectEventPersonByIds.buildPreparedStatement(); + try { + fps.setBigintU(1, epersonId); + return fps.executeQuery().getNextRow(); + } finally { + fps.close(); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement(sql = "SELECT * FROM ~g~.EventPerson WHERE epersonID=?") + private StatementProvider sqlSelectEventPersonById; + + @Override + protected Map fetchBulk(Collection epersonIds) throws SQLException { + FlexPreparedStatement fps = this.sqlSelectEventPersonByIds.buildPreparedStatement(epersonIds); + try { + return fps.executeQuery().getAllRows("epersonID", BigInteger.class); + } finally { + fps.close(); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement(sql = "SELECT * FROM ~g~.EventPerson WHERE epersonID IN (??)") + private StatementProvider sqlSelectEventPersonByIds; }