added agenda job/api/support

This commit is contained in:
2023-06-03 17:03:56 -04:00
parent 693dac38a7
commit 81c479eb3a
6 changed files with 485 additions and 8 deletions

View File

@@ -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<Long, BigInteger> 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);
}
}
}

View File

@@ -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<DataSet> 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<DataSet> 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<Long, BigInteger> recipientIDs = EventAutolist.getInstance().getPersonIDMap("signed", eventID.longValue());
this.send(agenda, recipientIDs);
}
public void send(FlexMap agenda, Map<Long, BigInteger> 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<Long> 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<Long> text(long eventID, long documentID, Map<Long, BigInteger> recipientIDs, String subject)
throws SQLException, MessagingException {
this.logger.debug("Sending agenda links by text with document ID: {}", documentID);
Map<Long, Contact> recipients = Contact.getContactMapByIds(recipientIDs.keySet(), false, true);
String baseUrl = baseGolfUrl
+ "?n=documentAgenda&eventID="
+ eventID
+ "&documentID="
+ documentID
+ "&epersonID=";
Set<Long> textedPersonIDs = new HashSet<Long>(recipientIDs.size());
for (Entry<Long, Contact> 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<Long, BigInteger> recipientIDs,
Set<Long> excludePersonIDs, String subject) throws SQLException, MessagingException {
this.logger.debug("Sending agenda links by email with document ID: {}", documentID);
Map<Long, Contact> recipients = Contact.getContactMapByIds(recipientIDs.keySet(), true, false);
String baseUrl = this.baseGolfUrl
+ "?n=documentAgenda&eventID="
+ eventID
+ "&documentID="
+ documentID
+ "&epersonID=";
for (Entry<Long, Contact> recipient : recipients.entrySet()) {
if (excludePersonIDs.contains(recipient.getKey()))
continue;
if (logger.isDebugEnabled())
logger.debug("Sending agenda to: "
+ recipient.getKey());
String message = "<html><body><p><a href=\""
+ baseUrl
+ "\">"
+ baseUrl
+ recipientIDs.get(recipient.getKey())
+ "</a></p></body></html>";
Emailer.getInstance(this.envConfig).send(Arrays.asList(recipient.getValue()), subject, message);
}
}
private void email(long eventID, long documentID, Map<Long, BigInteger> recipientIDs,
Set<Long> excludePersonIDs, String subject) throws SQLException, MessagingException, IOException {
this.logger.debug("Sending agenda contents with document ID: {}", documentID);
Map<Long, Contact> 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<Long, Contact> 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;
}

View File

@@ -0,0 +1,17 @@
package com.poststats.golf.service;
import com.brianlong.util.FlexMap;
import com.poststats.service.CacheableService;
public interface EventDocumentService extends CacheableService<Long> {
/**
* 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);
}

View File

@@ -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<BigInteger> {
FlexMap get(BigInteger epersonId);
FlexMap get(long eventId, long personId);
List<? extends FlexMap> getPeople(long eventId);

View File

@@ -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<Long> 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<Long, DataSet> fetchBulk(Collection<Long> 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;
}

View File

@@ -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<BigInteger> 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<? extends FlexMap> 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<BigInteger, DataSet> fetchBulk(Collection<BigInteger> 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;
}