diff --git a/src/main/java/com/poststats/golf/api/EventRoundApi.java b/src/main/java/com/poststats/golf/api/EventRoundApi.java index 9a406fd..55b7d6a 100644 --- a/src/main/java/com/poststats/golf/api/EventRoundApi.java +++ b/src/main/java/com/poststats/golf/api/EventRoundApi.java @@ -2,11 +2,16 @@ package com.poststats.golf.api; import com.brianlong.util.FlexMap; import com.poststats.api.Constants; +import com.poststats.formatter.PersonFormatter; +import com.poststats.formatter.PersonsFormatter; import com.poststats.golf.api.model.EventRound; import com.poststats.golf.api.model.EventRoundPairing; +import com.poststats.golf.api.model.EventRoundPairingOrder; +import com.poststats.golf.formatter.GolfCourseFormatter; import com.poststats.golf.service.CourseService; import com.poststats.golf.service.EventRoundPairingService; import com.poststats.golf.service.EventRoundService; +import com.poststats.golf.service.PersonService; import com.poststats.transformer.impl.DaoConverter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -19,11 +24,24 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; import java.util.List; +import java.util.ListIterator; import java.util.Map; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +58,9 @@ public class EventRoundApi { @PathParam("eventId") private long eventId; + @Inject + private PersonService golferService; + @Inject private EventRoundService roundService; @@ -139,7 +160,7 @@ public class EventRoundApi { @ApiResponse(responseCode = "200", description = "Success"), @ApiResponse( responseCode = "404", - description = "An event with the specified ID or any event rounds could not be found" + description = "An event or its round with the specified ID could not be found" ) }) public List getPairings(@PathParam("eroundId") @@ -152,6 +173,51 @@ public class EventRoundApi { return this.converter.convertValue(rows, EventRoundPairing.class); } + @GET + @Path("/round/{eroundId:[0-9]+}/pairings/csv") + @Produces("text/csv") + @Operation(summary = "Retrieves limited meta-data about all pairings for a single event round in the CSV format.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Success"), + @ApiResponse( + responseCode = "404", + description = "An event or its round with the specified ID could not be found" + ) + }) + public StreamingOutput getPairingsAsCsv(@PathParam("eroundId") + long eroundId, @QueryParam("orderBy") + List orderBys, @QueryParam("lastNameFirst") + boolean lastNameFirst) { + this.logger.debug("getPairingsAsCsv({}, {})", eroundId, lastNameFirst); + + List rows = this.pairingService.getParticipantsByRoundId(eroundId); + if (rows == null || rows.isEmpty()) + throw new WebApplicationException("No pairings found", Status.NOT_FOUND); + this.logger.debug("Found {} pairings in round: {}", rows.size(), eroundId); + + this.golferService.injectDeep("personID", rows, "golfer"); + + PersonFormatter personFormatter = new PersonFormatter(); + personFormatter.withNameReversal(lastNameFirst); + GolfCourseFormatter courseFormatter = new GolfCourseFormatter(); + + for (FlexMap row : rows) { + FlexMap golfer = row.get("golfer", FlexMap.class); + FlexMap person = golfer.get("person", FlexMap.class); + row.put("name", personFormatter.format(person)); + + FlexMap pairing = row.get("pairing", FlexMap.class); + this.courseService.inject("courseID", pairing, "course"); + + FlexMap course = pairing.get("course", FlexMap.class); + if (course != null) + pairing.put("courseName", courseFormatter.format(course)); + } + + this.sort(rows, orderBys); + return this.toCsv(rows, lastNameFirst); + } + @GET @Path("/round/{eroundId:[0-9]+}/pairing/{pairingID:[0-9]+}") @Produces(Constants.V1_JSON) @@ -179,4 +245,135 @@ public class EventRoundApi { return this.converter.convertValue(row, EventRoundPairing.class); } + private void sort(List persons, List orderBys) { + final List orders = new LinkedList<>((orderBys == null || orderBys.isEmpty()) + ? Arrays.asList(EventRoundPairingOrder.Pairing, EventRoundPairingOrder.FormattedName) + : orderBys); + this.logger.debug("Sorting by: {}", orders); + + ListIterator i = orders.listIterator(); + while (i.hasNext()) { + EventRoundPairingOrder order = i.next(); + if (order == EventRoundPairingOrder.Pairing) { + i.remove(); + i.add(EventRoundPairingOrder.TeeTime); + i.add(EventRoundPairingOrder.Course); + i.add(EventRoundPairingOrder.CourseNine); + i.add(EventRoundPairingOrder.HoleNumber); + } + } + + Collections.sort(persons, new Comparator() { + @Override + public int compare(FlexMap person1, FlexMap person2) { + if (person1 == null && person2 == null) { + return 0; + } else if (person1 == null) { + return -1; + } else if (person2 == null) { + return 1; + } else { + for (EventRoundPairingOrder order : orders) { + int compare = 0; + FlexMap pairing1 = person1.get("pairing", FlexMap.class); + FlexMap pairing2 = person2.get("pairing", FlexMap.class); + + switch (order) { + case FormattedName: + compare = person1.getString("name").compareTo(person2.getString("name")); + break; + case LastName: + compare = person1.get("golfer", FlexMap.class).get("person", FlexMap.class) + .getString("lname").compareTo(person2.get("golfer", FlexMap.class) + .get("person", FlexMap.class).getString("lname")); + break; + case FirstName: + compare = person1.get("golfer", FlexMap.class).get("person", FlexMap.class) + .getString("fname").compareTo(person2.get("golfer", FlexMap.class) + .get("person", FlexMap.class).getString("fname")); + break; + case Course: + if (!pairing1.isNotEmpty("course") && !pairing2.isNotEmpty("course")) { + } else if (!pairing1.isNotEmpty("course")) { + return -1; + } else if (!pairing2.isNotEmpty("course")) { + return 1; + } else { + compare = StringUtils.compare( + pairing1.get("course", FlexMap.class).getString("nine"), + pairing2.get("course", FlexMap.class).getString("nine")); + } + break; + case CourseNine: + if (!pairing1.isNotEmpty("nine") && !pairing2.isNotEmpty("nine")) { + } else if (!pairing1.isNotEmpty("nine")) { + return -1; + } else if (!pairing2.isNotEmpty("nine")) { + return 1; + } else { + compare = StringUtils.compare(pairing1.get("nine", FlexMap.class).getString("nine"), + pairing2.get("nine", FlexMap.class).getString("nine")); + } + break; + case HoleNumber: + compare = compareTo(pairing1.getByte("number"), pairing2.getByte("number")); + break; + case TeeTime: + compare = compareTo(pairing1.getTime("teetime"), pairing2.getTime("teetime")); + break; + default: + throw new IllegalArgumentException(); + } + + if (compare != 0) + return compare; + } + + return 0; + } + } + }); + } + + private StreamingOutput toCsv(List persons, boolean lastNameFirst) { + return new StreamingOutput() { + @Override + public void write(OutputStream output) throws IOException { + PersonsFormatter personsFormatter = new PersonsFormatter<>(); + personsFormatter.withNameReversal(lastNameFirst); + + PrintStream pstream = new PrintStream(output); + CSVPrinter personsCsvPrinter = new CSVPrinter(pstream, CSVFormat.DEFAULT); + try { + personsCsvPrinter.printRecord("Name", "Tee Time", "Course", "Course Nine", "Hole Number"); + + for (FlexMap eperson : persons) { + FlexMap pairing = eperson.get("pairing", FlexMap.class); + FlexMap nine = pairing.get("nine", FlexMap.class); + + personsCsvPrinter.printRecord(eperson.getString("name"), pairing.getTime("teetime"), + pairing.getString("courseName"), nine == null ? null : nine.getString("nine"), + pairing.getByte("number")); + } + + personsCsvPrinter.flush(); + } finally { + personsCsvPrinter.close(); + } + } + }; + } + + private > int compareTo(T c1, T c2) { + if (c1 == null && c2 == null) { + return 0; + } else if (c1 == null) { + return -1; + } else if (c2 == null) { + return 1; + } else { + return c1.compareTo(c2); + } + } + } diff --git a/src/main/java/com/poststats/golf/api/model/EventRoundPairingOrder.java b/src/main/java/com/poststats/golf/api/model/EventRoundPairingOrder.java new file mode 100644 index 0000000..452cb25 --- /dev/null +++ b/src/main/java/com/poststats/golf/api/model/EventRoundPairingOrder.java @@ -0,0 +1,20 @@ +package com.poststats.golf.api.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public enum EventRoundPairingOrder { + + @JsonProperty("name") + FormattedName, @JsonProperty("lastName") + LastName, @JsonProperty("firstName") + FirstName, @JsonProperty("teetime") + TeeTime, @JsonProperty("course") + Course, @JsonProperty("nine") + CourseNine, @JsonProperty("holeNumber") + HoleNumber, + + // an alias for teetime,course,nine,holeNumber + @JsonProperty("pairing") + Pairing + +} diff --git a/src/main/java/com/poststats/golf/job/EventAgendaJob.java b/src/main/java/com/poststats/golf/job/EventAgendaJob.java index 685d2d8..49c26b4 100644 --- a/src/main/java/com/poststats/golf/job/EventAgendaJob.java +++ b/src/main/java/com/poststats/golf/job/EventAgendaJob.java @@ -114,8 +114,9 @@ public class EventAgendaJob { 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) + boolean doTextLink = Boolean.TRUE.equals(agenda.getBoolean("sendTextWithLink")); + boolean doText = Boolean.TRUE.equals(agenda.getBoolean("sendTextWithContent")); + if (!doTextLink && !doText && !doEmailLink && !doEmail) return; Long eventId = agenda.getLong("eventID"); @@ -130,18 +131,21 @@ public class EventAgendaJob { + this.weekdayFormatter.format(day); Set textedPersonIDs = Collections.emptySet(); - if (doText) + + if (doTextLink) { + textedPersonIDs = this.textLink(eventId, documentId, recipientIDs, subject); + } else if (doText) { textedPersonIDs = this.text(eventId, documentId, recipientIDs, subject); + } - if (doEmailLink) + if (doEmailLink) { this.emailLink(eventId, documentId, recipientIDs, textedPersonIDs, subject); - - if (doEmail) { + } else if (doEmail) { this.email(eventId, documentId, recipientIDs, textedPersonIDs, subject); } } - private Set text(long eventID, long documentID, Map recipientIDs, String subject) + private Set textLink(long eventID, long documentID, Map recipientIDs, String subject) throws SQLException, MessagingException { this.logger.debug("Sending agenda links by text with document ID: {}", documentID); @@ -168,6 +172,45 @@ public class EventAgendaJob { return textedPersonIDs; } + private Set text(long eventID, long documentID, Map recipientIDs, String subject) + throws SQLException, MessagingException, IOException { + this.logger.debug("Sending agenda links by text with document ID: {}", documentID); + + List recipients = this.personService.getContacts(recipientIDs.keySet(), false, true); + + NameValuePair[] params = new NameValuePair[] { + new BasicNameValuePair("n", "documentAgendaMarkdown"), + new BasicNameValuePair("eventID", String.valueOf(eventID)), + new BasicNameValuePair("documentID", String.valueOf(documentID)) + }; + + Set textedPersonIDs = new HashSet(recipientIDs.size()); + + for (Contact recipient : recipients) { + long personId = recipient.getPersonId(); + this.logger.debug("Sending agenda to: {}", personId); + + HttpUriRequest request = RequestBuilder.get(this.baseGolfUrl).addParameters(params) + .addParameter("epersonID", recipientIDs.get(personId).toString()).build(); + + CloseableHttpClient hclient = HttpClientBuilder.create().build(); + CloseableHttpResponse response = hclient.execute(request); + try { + if (response.getStatusLine().getStatusCode() / 100 == 2) { + String markdown = IOUtils.toString(response.getEntity().getContent(), "utf-8"); + this.texter.send(Arrays.asList(recipient), subject, markdown.trim()); + textedPersonIDs.add(personId); + } else { + this.logger.warn("The URL could not be loaded properly: {}", request.getURI()); + } + } finally { + response.close(); + } + } + + 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); diff --git a/src/main/java/com/poststats/golf/service/EventRoundPairingService.java b/src/main/java/com/poststats/golf/service/EventRoundPairingService.java index 0ab73ea..8047cb4 100644 --- a/src/main/java/com/poststats/golf/service/EventRoundPairingService.java +++ b/src/main/java/com/poststats/golf/service/EventRoundPairingService.java @@ -10,4 +10,6 @@ public interface EventRoundPairingService { List getByRoundId(long eroundId); + List getParticipantsByRoundId(long eroundId); + } diff --git a/src/main/java/com/poststats/golf/service/db/EventRoundPairingServiceDAO.java b/src/main/java/com/poststats/golf/service/db/EventRoundPairingServiceDAO.java index 500e4e2..5d68849 100644 --- a/src/main/java/com/poststats/golf/service/db/EventRoundPairingServiceDAO.java +++ b/src/main/java/com/poststats/golf/service/db/EventRoundPairingServiceDAO.java @@ -72,4 +72,35 @@ public class EventRoundPairingServiceDAO implements EventRoundPairingService { ) private StatementProvider sqlSelectByRoundId; + @Override + public List getParticipantsByRoundId(long eroundId) { + try { + FlexPreparedStatement fps = this.sqlSelectParticipantsByRoundId.buildPreparedStatement(); + try { + fps.setIntegerU(1, eroundId); + return fps.executeQuery().getAllRows(new FlexManyToOneDef("pairingID", "pairing"), + new FlexManyToOneDef("nineID", "pairing", "nine")); + } finally { + fps.close(); + } + } catch (SQLException se) { + throw new ServiceException(se); + } + } + + @Inject + @NonTransactionalProvider + @GolfProvider + @Statement( + sql = "SELECT EPR.epersonID, EP.personID, " + + " ERP.pairingID, ERP.teetime, ERP.eroundID, ERP.courseID, ERP.number, " + + " CN.nineID, CN.nine, CN.courseID " + + "FROM ~g~.EventRoundPairing ERP " + + " INNER JOIN ~g~.EventPersonRound EPR ON (ERP.pairingID=EPR.pairingID) " + + " INNER JOIN ~g~.EventPerson EP ON (EPR.epersonID=EP.epersonID) " + + " LEFT JOIN ~g~.CourseNine CN ON (ERP.nineID=CN.nineID) " + + "WHERE ERP.eroundID=? " + ) + private StatementProvider sqlSelectParticipantsByRoundId; + }