package gov.nysenate.openleg.service.spotcheck.calendar; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import gov.nysenate.openleg.model.base.Version; import gov.nysenate.openleg.model.calendar.Calendar; import gov.nysenate.openleg.model.calendar.*; import gov.nysenate.openleg.model.spotcheck.*; import gov.nysenate.openleg.service.spotcheck.base.SpotCheckService; import gov.nysenate.openleg.util.DateUtils; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.*; @Service public class CalendarCheckService implements SpotCheckService<CalendarId, Calendar, Calendar> { @Override public SpotCheckObservation<CalendarId> check(Calendar content) throws ReferenceDataNotFoundEx { throw new NotImplementedException(":P"); } @Override public SpotCheckObservation<CalendarId> check(Calendar content, LocalDateTime start, LocalDateTime end) throws ReferenceDataNotFoundEx { throw new NotImplementedException(":P"); } @Override public SpotCheckObservation<CalendarId> check(Calendar content, Calendar reference) { SpotCheckObservation<CalendarId> observation = initializeObservation(reference); if (calendarsAreEqual(content, reference)) { return observation; } else { compareSupplementals(observation, content, reference); compareActiveLists(observation, content, reference); return observation; } } private SpotCheckObservation<CalendarId> initializeObservation(Calendar reference) { SpotCheckReferenceId referenceId = new SpotCheckReferenceId( SpotCheckRefType.LBDC_CALENDAR_ALERT, reference.getPublishedDateTime()); return new SpotCheckObservation<>(referenceId, reference.getId()); } /** * Compare Calendar equality, ignoring published date. * @return <code>true</code> if calendar's id's, supplemental's, and active list's are equal. <code>false</code> otherwise. */ private boolean calendarsAreEqual(Calendar content, Calendar other) { return Objects.equals(content.getId(), other.getId()) && Objects.equals(content.getSupplementalMap(), other.getSupplementalMap()) && Objects.equals(content.getActiveListMap(), other.getActiveListMap()); } private void compareSupplementals(SpotCheckObservation<CalendarId> observation, Calendar content, Calendar reference) { Set<CalendarSupplemental> contentSupplementals = ImmutableSet.copyOf(content.getSupplementalMap().values()); Set<CalendarSupplemental> referenceSupplementals = ImmutableSet.copyOf(reference.getSupplementalMap().values()); Set<CalendarSupplemental> differenceSet = Sets.symmetricDifference(contentSupplementals, referenceSupplementals).immutableCopy(); for (CalendarSupplemental diff : differenceSet) { recordMismatch(observation, content, reference, diff); } } private void recordMismatch(SpotCheckObservation<CalendarId> observation, Calendar content, Calendar reference, CalendarSupplemental diff) { CalendarSupplemental contentSuppDiff = getMatchingSupplementalIfExists(content, diff); CalendarSupplemental referenceSuppDiff = getMatchingSupplementalIfExists(reference, diff); // They should never both be null, if they are, no mismatch to record. if (contentSuppDiff == null && referenceSuppDiff == null) { return; } if (contentSuppDiff == null) { recordObservationDataMismatch(observation, referenceSuppDiff); } else if (referenceSuppDiff == null) { recordReferenceDataMismatch(observation, contentSuppDiff); } else { checkForSupplementalCalDateMismatch(observation, contentSuppDiff, referenceSuppDiff); Set<CalendarSectionType> contentSectionTypes = contentSuppDiff.getSectionEntries().keySet(); Set<CalendarSectionType> referenceSectionTypes = referenceSuppDiff.getSectionEntries().keySet(); checkForTypeMismatch(observation, contentSectionTypes, referenceSectionTypes); checkForSuppEntryMismatch(observation, contentSuppDiff, referenceSuppDiff); } } private void recordObservationDataMismatch(SpotCheckObservation<CalendarId> observation, CalendarSupplemental reference) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.OBSERVE_DATA_MISSING, "", getVersionString(reference))); } private void recordReferenceDataMismatch(SpotCheckObservation<CalendarId> observation, CalendarSupplemental content) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.REFERENCE_DATA_MISSING, getVersionString(content), "")); } /** * Converts a supplemental's {@link Version} into a more informative string for mismatches. * <p>Returns:</p> * <ul> * <li>Empty string if the supplemental is null</li> * <li>"BASE" if the Version = Version.BASE</li> * </ul> * This is different from the Version.toString() behavior of returning an empty string for the Default/Base version. */ private String getVersionString(CalendarSupplemental contentSuppDiff) { String versionString = ""; if (contentSuppDiff != null) { Version v = contentSuppDiff.getVersion(); versionString = v.equals(Version.DEFAULT) ? "BASE" : v.toString(); } return versionString; } private void checkForSupplementalCalDateMismatch(SpotCheckObservation<CalendarId> observation, CalendarSupplemental contentSuppDiff, CalendarSupplemental referenceSuppDiff) { String contentDate = contentSuppDiff.getCalDate() == null ? "" : contentSuppDiff.getCalDate().toString(); String referenceDate = referenceSuppDiff.getCalDate() == null ? "" : referenceSuppDiff.getCalDate().toString(); if (!StringUtils.equals(contentDate, referenceDate)) { observation.addMismatch(new SpotCheckMismatch(SpotCheckMismatchType.SUPPLEMENTAL_CAL_DATE, contentDate, referenceDate)); } } private void checkForTypeMismatch(SpotCheckObservation<CalendarId> observation, Set<CalendarSectionType> contentSectionTypes, Set<CalendarSectionType> referenceSectionTypes) { if (!Sets.symmetricDifference(contentSectionTypes, referenceSectionTypes).isEmpty()) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.SUPPLEMENTAL_SECTION_TYPE, StringUtils.join(contentSectionTypes, "\n"), StringUtils.join(referenceSectionTypes, "\n") )); } } private void checkForSuppEntryMismatch(SpotCheckObservation<CalendarId> observation, CalendarSupplemental contentSuppDiff, CalendarSupplemental referenceSuppDiff) { Map<String, CalendarSupplementalEntry> contentEntryMap = getStringToEntryMap(contentSuppDiff); Map<String, CalendarSupplementalEntry> referenceEntryMap = getStringToEntryMap(referenceSuppDiff); Set<String> entryDiffs = Sets.symmetricDifference(contentEntryMap.keySet(), referenceEntryMap.keySet()); for (String diff : entryDiffs) { CalendarSupplementalEntry contentDiff = contentEntryMap.get(diff); CalendarSupplementalEntry referenceDiff = referenceEntryMap.get(diff); observation.addMismatch(new SpotCheckMismatch(SpotCheckMismatchType.SUPPLEMENTAL_ENTRY, contentDiff == null ? "" : contentDiff.toString(), referenceDiff == null ? "" : referenceDiff.toString() )); } } private Map<String, CalendarSupplementalEntry> getStringToEntryMap(CalendarSupplemental supplemental) { Map<String, CalendarSupplementalEntry> stringToEntryMap = new HashMap<>(); for (CalendarSupplementalEntry entry : supplemental.getAllEntries()) { stringToEntryMap.put(entry.toString(), entry); } return stringToEntryMap; } /** * Returns the matching supplemental from a calendar, null if it no match exists. */ private CalendarSupplemental getMatchingSupplementalIfExists(Calendar calendar, CalendarSupplemental diff) { return calendar.getSupplementalMap().keySet().contains(diff.getVersion()) ? calendar.getSupplemental(diff.getVersion()) : null; } /** * Does NOT do a full comparison of active lists. * We only compare the most recent (by release date time) versions against each other. * This is done because we don't get sequence num info in alert emails. Active lists in alert emails * can either be new supplementals(with an incremented sequence num) or an update to a previous supplemental, * but we can't tell which. * * @param observation * @param content * @param reference */ private void compareActiveLists(SpotCheckObservation<CalendarId> observation, Calendar content, Calendar reference) { TreeMap<Integer, CalendarActiveList> contentActiveListMap = content.getActiveListMap(); TreeMap<Integer, CalendarActiveList> referenceActiveListMap = reference.getActiveListMap(); if (contentActiveListMap.size() == 0 && referenceActiveListMap.size() == 0) { return; // No mismatches. } if (contentActiveListMap.size() == 0) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.OBSERVE_DATA_MISSING, "", referenceActiveListMap.get(referenceActiveListMap.size() - 1).getSequenceNo())); } else if (referenceActiveListMap.size() == 0) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.REFERENCE_DATA_MISSING, contentActiveListMap.get(contentActiveListMap.size() - 1).getSequenceNo(), "")); } else { CalendarActiveList contentMostRecent = getMostRecentActiveList(content); CalendarActiveList referenceMostRecent = getMostRecentActiveList(reference); checkForActiveListCalDateMismatch(observation, contentMostRecent, referenceMostRecent); checkForActiveListEntryMismatch(observation, contentMostRecent, referenceMostRecent); } } private CalendarActiveList getMostRecentActiveList(Calendar calendar) { int seqNo = 0; LocalDateTime releaseDateTime = DateUtils.LONG_AGO.atStartOfDay(); for (CalendarActiveList activeList : calendar.getActiveListMap().values()) { if (activeList.getReleaseDateTime().isAfter(releaseDateTime)) { seqNo = activeList.getSequenceNo(); releaseDateTime = activeList.getReleaseDateTime(); } } return calendar.getActiveList(seqNo); } private void checkForActiveListCalDateMismatch(SpotCheckObservation<CalendarId> observation, CalendarActiveList contentDiff, CalendarActiveList referenceDiff) { String contentCalDate = contentDiff.getCalDate() == null ? "" : contentDiff.getCalDate().toString(); String referenceCalDate = referenceDiff.getCalDate() == null ? "" : referenceDiff.getCalDate().toString(); if (!StringUtils.equals(contentCalDate, referenceCalDate)) { observation.addMismatch(new SpotCheckMismatch(SpotCheckMismatchType.ACTIVE_LIST_CAL_DATE, contentCalDate, referenceCalDate)); } } private void checkForActiveListEntryMismatch(SpotCheckObservation<CalendarId> observation, CalendarActiveList contentDiff, CalendarActiveList referenceDiff) { Set<CalendarEntry> contentDiffEntries = Sets.newHashSet(contentDiff.getEntries()); Set<CalendarEntry> referenceDiffEntries = Sets.newHashSet(referenceDiff.getEntries()); if (!Sets.symmetricDifference(contentDiffEntries, referenceDiffEntries).isEmpty()) { observation.addMismatch(new SpotCheckMismatch( SpotCheckMismatchType.ACTIVE_LIST_ENTRY, StringUtils.join(contentDiffEntries, "\n"), StringUtils.join(referenceDiffEntries, "\n"))); } } }