package gov.nysenate.openleg.dao.agenda.reference; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Range; import com.google.common.io.Files; import gov.nysenate.openleg.config.Environment; import gov.nysenate.openleg.dao.base.LimitOffset; import gov.nysenate.openleg.dao.base.SqlBaseDao; import gov.nysenate.openleg.dao.bill.reference.daybreak.SqlFsDaybreakDao; import gov.nysenate.openleg.model.agenda.AgendaInfoCommitteeItem; import gov.nysenate.openleg.model.spotcheck.agenda.AgendaAlertId; import gov.nysenate.openleg.model.spotcheck.agenda.AgendaAlertInfoCommId; import gov.nysenate.openleg.model.spotcheck.agenda.AgendaAlertInfoCommittee; import gov.nysenate.openleg.model.base.Version; import gov.nysenate.openleg.model.bill.BillId; import gov.nysenate.openleg.model.entity.Chamber; import gov.nysenate.openleg.model.entity.CommitteeId; import gov.nysenate.openleg.model.spotcheck.SpotCheckRefType; import gov.nysenate.openleg.model.spotcheck.SpotCheckReferenceId; import gov.nysenate.openleg.util.DateUtils; import gov.nysenate.openleg.util.FileIOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import javax.annotation.PostConstruct; import java.io.File; import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; import static gov.nysenate.openleg.dao.agenda.reference.SqlAgendaAlertQuery.*; @Repository public class SqlFsAgendaAlertDao extends SqlBaseDao implements AgendaAlertDao { private static final Logger logger = LoggerFactory.getLogger(SqlFsDaybreakDao.class); private static final Pattern agendaFilePattern = Pattern.compile("^agenda_alert-\\d{8}-[A-z\\._]+-[A-z]+-?\\d{8}T\\d{6}.html$"); // This pattern parses both full and individual agenda alert filenames, but currently we can't reliably process full alerts private static final Pattern agendaFullFilePattern = Pattern.compile("^agenda_alert-\\d{8}-[A-z\\._-]*\\d{8}T\\d{6}.html$"); @Autowired private Environment environment; private File incomingAgendaAlertDir; private File archiveAgendaAlertDir; @PostConstruct public void init() { incomingAgendaAlertDir = new File(environment.getStagingDir(), "alerts"); archiveAgendaAlertDir = new File(environment.getArchiveDir(), "alerts"); } /** {@inheritDoc} */ @Override public List<File> getIncomingAgendaAlerts() throws IOException { List<File> agendaAlerts = FileIOUtils.safeListFiles(incomingAgendaAlertDir, null, false).stream() .filter(file -> agendaFullFilePattern.matcher(file.getName()).matches()) .collect(Collectors.toList()); List<File> singleAgendaAlerts = new ArrayList<>(); for (File alert : agendaAlerts) { if (agendaFilePattern.matcher(alert.getName()).matches()) { singleAgendaAlerts.add(alert); } else { // Archive the full agenda alerts archiveAgendaAlert(alert); } } return singleAgendaAlerts; } /** {@inheritDoc} */ @Override public void archiveAgendaAlert(File agendaAlert) throws IOException { Files.move(agendaAlert, new File(archiveAgendaAlertDir, agendaAlert.getName())); } /** {@inheritDoc} */ @Override public AgendaAlertInfoCommittee getAgendaAlertInfoCommittee(AgendaAlertInfoCommId agendaCommInfoId) { AgendaAlertInfoCommRowHandler rowHandler = new AgendaAlertInfoCommRowHandler(); jdbcNamed.query(SELECT_INFO_COMMITTEE_BY_ID.getSql(schema(), LimitOffset.ONE), getAgendaAlertInfoCommIdParams(agendaCommInfoId), rowHandler); List<AgendaAlertInfoCommittee> result = rowHandler.getAlertInfoCommittees(); if (result.size() == 1) { return result.get(0); } if (result.isEmpty()) { throw new EmptyResultDataAccessException(1); } throw new IncorrectResultSizeDataAccessException(1, result.size()); } /** {@inheritDoc} */ @Override public List<AgendaAlertInfoCommittee> getAgendaAlertReferences(Range<LocalDateTime> dateTimeRange) { AgendaAlertInfoCommRowHandler rowHandler = new AgendaAlertInfoCommRowHandler(); jdbcNamed.query(SELECT_IN_RANGE.getSql(schema()), getDateTimeRangeParams(dateTimeRange), rowHandler); return rowHandler.getAlertInfoCommittees(); } /** {@inheritDoc} */ @Override public List<AgendaAlertInfoCommittee> getUncheckedAgendaAlertReferences() { AgendaAlertInfoCommRowHandler rowHandler = new AgendaAlertInfoCommRowHandler(); jdbcNamed.query(SELECT_UNCHECKED.getSql(schema()), rowHandler); return rowHandler.getAlertInfoCommittees(); } @Override public List<AgendaAlertInfoCommittee> getProdUncheckedAgendaAlertReferences() { AgendaAlertInfoCommRowHandler rowHandler = new AgendaAlertInfoCommRowHandler(); jdbcNamed.query(SELECT_PROD_UNCHECKED.getSql(schema()), rowHandler); return groupAlertInfoCommittees(rowHandler.getAlertInfoCommittees()); } @Override public List<AgendaAlertInfoCommittee> getProdAgendaAlertReferences(Range<LocalDateTime> dateTimeRange) { AgendaAlertInfoCommRowHandler rowHandler = new AgendaAlertInfoCommRowHandler(); jdbcNamed.query(SELECT_IN_RANGE.getSql(schema()), getDateTimeRangeParams(dateTimeRange), rowHandler); return groupAlertInfoCommittees(rowHandler.getAlertInfoCommittees()); } /** {@inheritDoc} */ @Override public void updateAgendaAlertInfoCommittee(AgendaAlertInfoCommittee aaic) { deleteAAIC(aaic.getAgendaAlertInfoCommId()); KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcNamed.update(INSERT_INFO_COMMITTEE.getSql(schema()), getAgendaAlertInfoCommParams(aaic), keyHolder, new String[]{"id"}); aaic.getItems().forEach(item -> insertAAICItem(item, keyHolder.getKey().intValue())); } /** {@inheritDoc} */ @Override public void setAgendaAlertChecked(AgendaAlertInfoCommId agendaAlertId, boolean checked) { MapSqlParameterSource params = getAgendaAlertInfoCommIdParams(agendaAlertId); params.addValue("checked", checked); jdbcNamed.update(SET_INFO_COMMITTEE_CHECKED.getSql(schema()), params); } /** {@inheritDoc} */ @Override public void setAgendaAlertProdChecked(AgendaAlertInfoCommittee alertInfoCommittee, boolean checked) { MapSqlParameterSource params = getAgendaAlertInfoCommParams(alertInfoCommittee) .addValue("checked", checked); jdbcNamed.update(SET_MEETING_PROD_CHECKED.getSql(schema()), params); } /** --- Internal Methods --- */ /** * Deletes an AgendaAlertInfoCommittee * @param aaicID AgendaAlertInfoCommId */ private void deleteAAIC(AgendaAlertInfoCommId aaicID) { jdbcNamed.update(DELETE_INFO_COMMITTEE.getSql(schema()), getAgendaAlertInfoCommIdParams(aaicID)); } /** * Inserts an AgendaInfoCommitteeItem under the given AgendaAlertInfoCommittee row id * @param aici AgendaInfoCommitteeItem * @param aaicId int - row id for an AgendaAlertInfoCommittee */ private void insertAAICItem(AgendaInfoCommitteeItem aici, int aaicId) { jdbcNamed.update(INSERT_INFO_COMMITTEE_ITEM.getSql(schema()), getAgendaInfoCommItemParams(aici, aaicId)); } private List<AgendaAlertInfoCommittee> groupAlertInfoCommittees(List<AgendaAlertInfoCommittee> alertInfoCommittees) { Map<CommitteeId, ArrayListMultimap<LocalDate, AgendaAlertInfoCommittee>> aaicMap = new HashMap<>(); alertInfoCommittees.forEach(aaic -> { if (!aaicMap.containsKey(aaic.getCommitteeId())) { aaicMap.put(aaic.getCommitteeId(), ArrayListMultimap.create()); } aaicMap.get(aaic.getCommitteeId()).put(aaic.getMeetingDateTime().toLocalDate(), aaic); }); return aaicMap.values().stream() .flatMap(multiMap -> multiMap.keySet().stream().map(multiMap::get)) .filter(aaicList -> !aaicList.isEmpty()) .map(aaicList -> aaicList.stream().reduce(null, AgendaAlertInfoCommittee::merge)) .collect(Collectors.toList()); } /** --- Row Mappers --- */ private static final RowMapper<AgendaAlertInfoCommittee> agendaAlertInfoCommRowMapper = (rs, rowNum) -> { AgendaAlertInfoCommittee aaic = new AgendaAlertInfoCommittee(); aaic.setReferenceId(new SpotCheckReferenceId( SpotCheckRefType.LBDC_AGENDA_ALERT, getLocalDateTimeFromRs(rs, "reference_date_time") )); aaic.setWeekOf(getLocalDateFromRs(rs, "week_of")); aaic.setAddendum(Version.of(rs.getString("addendum_id"))); aaic.setCommitteeId(new CommitteeId( Chamber.getValue(rs.getString("chamber")), rs.getString("committee_name") )); aaic.setChair(rs.getString("chair")); aaic.setLocation(rs.getString("location")); aaic.setMeetingDateTime(getLocalDateTimeFromRs(rs, "meeting_date_time")); aaic.setNotes(rs.getString("notes")); return aaic; }; private static final RowMapper<AgendaInfoCommitteeItem> agendaInfoCommItemRowMapper = (rs, rowNum) -> { AgendaInfoCommitteeItem item = new AgendaInfoCommitteeItem(); item.setBillId(new BillId(rs.getString("bill_print_no"), rs.getInt("bill_session_year"), rs.getString("bill_amend_version"))); item.setMessage(rs.getString("message")); return item; }; private static class AgendaAlertInfoCommRowHandler implements RowCallbackHandler { private Map<Integer, AgendaAlertInfoCommittee> committeeMeetingRefMap = new HashMap<>(); @Override public void processRow(ResultSet rs) throws SQLException { int aaicId = rs.getInt("id"); int itemId = rs.getInt("alert_info_committee_id"); if (!committeeMeetingRefMap.containsKey(aaicId)) { committeeMeetingRefMap.put(aaicId, agendaAlertInfoCommRowMapper.mapRow(rs, rs.getRow())); } if (itemId != 0) { committeeMeetingRefMap.get(aaicId).addInfoCommitteeItem( agendaInfoCommItemRowMapper.mapRow(rs, rs.getRow())); } } public List<AgendaAlertInfoCommittee> getAlertInfoCommittees() { return new ArrayList<>(committeeMeetingRefMap.values()); } } /** --- Parameter Mappers --- */ private MapSqlParameterSource getAgendaAlertIdParams(AgendaAlertId alertId) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("referenceDateTime", DateUtils.toDate(alertId.getReferenceDateTime())); params.addValue("weekOf", DateUtils.toDate(alertId.getWeekOf())); return params; } private MapSqlParameterSource getAgendaAlertInfoCommIdParams(AgendaAlertInfoCommId commId) { MapSqlParameterSource params = getAgendaAlertIdParams(commId); params.addValue("addendumId", commId.getAddendum().getValue()); params.addValue("chamber", commId.getCommitteeId().getChamber().asSqlEnum()); params.addValue("committeeName", commId.getCommitteeId().getName()); return params; } private MapSqlParameterSource getAgendaAlertInfoCommParams(AgendaAlertInfoCommittee aaic) { MapSqlParameterSource params = getAgendaAlertInfoCommIdParams(aaic.getAgendaAlertInfoCommId()); params.addValue("chair", aaic.getChair()); params.addValue("location", aaic.getLocation()); params.addValue("meetingDateTime", DateUtils.toDate(aaic.getMeetingDateTime())); params.addValue("notes", aaic.getNotes()); return params; } private MapSqlParameterSource getAgendaInfoCommItemParams(AgendaInfoCommitteeItem aici, int aaicId) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("alertInfoCommitteeId", aaicId); params.addValue("billPrintNo", aici.getBillId().getBasePrintNo()); params.addValue("billSessionYear", aici.getBillId().getSession().getYear()); params.addValue("billAmendVersion", aici.getBillId().getVersion().getValue()); params.addValue("message", aici.getMessage()); return params; } }