package gov.nysenate.openleg.dao.spotcheck;
import com.google.common.collect.Sets;
import gov.nysenate.openleg.dao.base.*;
import gov.nysenate.openleg.model.base.SessionYear;
import gov.nysenate.openleg.model.spotcheck.*;
import gov.nysenate.openleg.service.spotcheck.base.MismatchNotFoundEx;
import gov.nysenate.openleg.service.spotcheck.base.MismatchStatusService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
import static gov.nysenate.openleg.dao.spotcheck.SqlSpotCheckReportQuery.*;
import static gov.nysenate.openleg.util.DateUtils.toDate;
/**
* The AbstractSpotCheckReportDao implements all the functionality required by SpotCheckReportDao
* regardless of the content key specified. This class must be subclasses with a concrete type for
* the ContentKey. The subclass will need to handle just the conversions for the ContentKey class.
*
* @param <ContentKey>
*/
public abstract class AbstractSpotCheckReportDao<ContentKey> extends SqlBaseDao
implements SpotCheckReportDao<ContentKey> {
private static final Logger logger = LoggerFactory.getLogger(AbstractSpotCheckReportDao.class);
/** --- Abstract Methods --- */
/**
* Subclasses should implement this conversion from a Map containing certain key/val pairs to
* an instance of ContentKey. This is needed since the keys are stored as an hstore in the
* database.
*
* @param keyMap Map<String, String>
* @return ContentKey
*/
public abstract ContentKey getKeyFromMap(Map<String, String> keyMap);
/**
* Subclasses should implement a conversion from an instance of ContentKey to a Map of
* key/val pairs that fully represent that ContentKey.
*
* @param key ContentKey
* @return Map<String, String>
*/
public abstract Map<String, String> getMapFromKey(ContentKey key);
/** --- Implemented Methods --- */
/**
* {@inheritDoc}
*/
@Override
public DeNormSpotCheckMismatch getMismatch(int mismatchId) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId);
String sql = SqlSpotCheckReportQuery.GET_MISMATCH.getSql(schema());
List<DeNormSpotCheckMismatch> results = jdbcNamed.query(sql, params, new MismatchMapper());
if (results.size() == 0) {
throw new MismatchNotFoundEx(mismatchId);
}
return results.get(0);
}
/**
* {@inheritDoc}
*/
@Override
public PaginatedList<DeNormSpotCheckMismatch> getMismatches(MismatchQuery query, LimitOffset limitOffset) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("datasource", query.getDataSource().name())
.addValue("contentTypes", query.getContentTypes().stream().map(Enum::name).collect(Collectors.toSet()))
.addValue("statuses", query.getMismatchStatuses().stream().map(Enum::name).collect(Collectors.toSet()))
.addValue("ignoreStatuses", query.getIgnoredStatuses().stream().map(Enum::name).collect(Collectors.toSet()))
.addValue("toDate", query.getToDate())
.addValue("fromDate", query.getFromDate());
String sql = SqlSpotCheckReportQuery.GET_MISMATCHES.getSql(schema(), query.getOrderBy(), limitOffset);
PaginatedRowHandler<DeNormSpotCheckMismatch> handler = new PaginatedRowHandler<>(limitOffset, "total_rows", new MismatchMapper());
jdbcNamed.query(sql, params, handler);
return handler.getList();
}
/**
* {@inheritDoc}
*/
@Override
public MismatchSummary getMismatchSummary(SpotCheckDataSource datasource, LocalDateTime summaryDateTime) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("datasource", datasource.name())
.addValue("fromDate", SessionYear.of(summaryDateTime.getYear()).asDateTimeRange().lowerEndpoint())
.addValue("toDate", summaryDateTime)
.addValue("startOfToDate", summaryDateTime.truncatedTo(ChronoUnit.DAYS));
String sql = SqlSpotCheckReportQuery.MISMATCH_SUMMARY.getSql(schema());
MismatchSummaryHandler summaryHandler = new MismatchSummaryHandler();
jdbcNamed.query(sql, params, summaryHandler);
return summaryHandler.getSummary();
}
/**
* {@inheritDoc}
*/
@Override
public void saveReport(SpotCheckReport<ContentKey> report) {
int reportId = insertReport(report);
// Return early if the observations have not been set
if (report.getObservations() == null) {
logger.warn("The observations have not been set on this report.");
return;
}
// Get the Keys and MismatchTypes checked in this report. (Used in calculating resolved mismatches)
Set<Object> checkedKeys = report.getObservations().values().stream().map(SpotCheckObservation::getKey).collect(Collectors.toSet());
Set<SpotCheckMismatchType> checkedTypes = report.getReferenceType().checkedMismatchTypes();
List<DeNormSpotCheckMismatch> reportMismatches = reportToDeNormMismatches(report, reportId);
MismatchQuery query = new MismatchQuery(report.getReferenceType().getDataSource(), Sets.newHashSet(report.getReferenceType().getContentType()))
.withToDate(report.getReferenceDateTime())
.withMismatchStatuses(EnumSet.allOf(SpotCheckMismatchStatus.class))
.withIgnoredStatuses(EnumSet.allOf(SpotCheckMismatchIgnore.class));
List<DeNormSpotCheckMismatch> currentMismatches = getMismatches(query, LimitOffset.ALL).getResults();
List<DeNormSpotCheckMismatch> updatedMismatches = MismatchStatusService.deriveStatuses(reportMismatches, currentMismatches);
updatedMismatches.addAll(MismatchStatusService.deriveResolved(reportMismatches, currentMismatches, checkedKeys,
checkedTypes, report.getReportDateTime(), report.getReferenceDateTime()));
insertMismatches(updatedMismatches);
}
private int insertReport(SpotCheckReport<ContentKey> report) {
ImmutableParams reportParams = ImmutableParams.from(getReportIdParams(report));
KeyHolder reportIdHolder = new GeneratedKeyHolder();
jdbcNamed.update(INSERT_REPORT.getSql(schema()), reportParams, reportIdHolder, new String[]{"id"});
return reportIdHolder.getKey().intValue();
}
private void insertMismatches(List<DeNormSpotCheckMismatch> mismatches) {
List<MapSqlParameterSource> params = mismatches.stream()
.map(this::mismatchParams)
.collect(Collectors.toList());
String sql = INSERT_MISMATCH.getSql(schema());
jdbcNamed.batchUpdate(sql, params.stream().toArray(MapSqlParameterSource[]::new));
}
private MapSqlParameterSource mismatchParams(DeNormSpotCheckMismatch mismatch) {
return new MapSqlParameterSource()
.addValue("key", toHstoreString(getMapFromKey((ContentKey) mismatch.getKey())))
.addValue("mismatchType", mismatch.getType().name())
.addValue("reportId", mismatch.getReportId())
.addValue("datasource", mismatch.getDataSource().name())
.addValue("contentType", mismatch.getContentType().name())
.addValue("referenceType", mismatch.getReferenceId().getReferenceType().name())
.addValue("mismatchStatus", mismatch.getStatus().name())
.addValue("referenceData", mismatch.getReferenceData())
.addValue("observedData", mismatch.getObservedData())
.addValue("notes", mismatch.getNotes())
.addValue("issueIds", toPostgresArray(mismatch.getIssueIds()))
.addValue("ignoreLevel", mismatch.getIgnoreStatus().name())
.addValue("reportDateTime", mismatch.getReportDateTime())
.addValue("observedDateTime", mismatch.getObservedDateTime())
.addValue("referenceActiveDateTime", mismatch.getReferenceId().getRefActiveDateTime());
}
private String toPostgresArray(Set<String> strings) {
return "{" + StringUtils.join(strings, ',') + "}";
}
private List<DeNormSpotCheckMismatch> reportToDeNormMismatches(SpotCheckReport<ContentKey> report, int reportId) {
List<DeNormSpotCheckMismatch> mismatches = new ArrayList<>();
for (SpotCheckObservation<ContentKey> ob : report.getObservations().values()) {
// Skip if no mismatches in the observation
if (ob.getMismatches().size() == 0) {
continue;
}
for (SpotCheckMismatch m : ob.getMismatches().values()) {
DeNormSpotCheckMismatch mismatch = new DeNormSpotCheckMismatch<>(ob.getKey(), m.getMismatchType(),
report.getReferenceType().getDataSource());
mismatch.setReportId(reportId);
mismatch.setContentType(report.getReferenceType().getContentType());
mismatch.setReferenceId(ob.getReferenceId());
mismatch.setReferenceData(m.getReferenceData());
mismatch.setObservedData(m.getObservedData());
mismatch.setNotes(m.getNotes());
mismatch.setObservedDateTime(ob.getObservedDateTime());
mismatch.setReportDateTime(report.getReportDateTime());
if (m.getIgnoreStatus() != null)
mismatch.setIgnoreStatus(m.getIgnoreStatus());
mismatch.setIssueIds(new HashSet<>(m.getIssueIds()));
mismatches.add(mismatch);
}
}
return mismatches;
}
/**
* {@inheritDoc}
*/
@Override
public void setMismatchIgnoreStatus(int mismatchId, SpotCheckMismatchIgnore ignoreStatus) {
if (ignoreStatus == null) {
throw new IllegalArgumentException("Cannot set mismatch ignore status to null.");
}
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId)
.addValue("ignoreStatus", ignoreStatus.name());
String sql = SqlSpotCheckReportQuery.UPDATE_MISMATCH_IGNORE.getSql(schema());
jdbcNamed.update(sql, params);
}
/**
* {@inheritDoc}
*/
@Override
public void updateIssueId(int mismatchId, String issueId) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId)
.addValue("issueId", issueId);
String sql = SqlSpotCheckReportQuery.UPDATE_ISSUE_ID.getSql(schema());
jdbcNamed.update(sql, params);
}
/**
* {@inheritDoc}
*/
@Override
public void addIssueId(int mismatchId, String issueId) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId)
.addValue("issueId", issueId);
String sql = SqlSpotCheckReportQuery.ADD_ISSUE_ID.getSql(schema());
jdbcNamed.update(sql, params);
}
/**
* {@inheritDoc}
*/
@Override
public void deleteIssueId(int mismatchId, String issueId) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId)
.addValue("issueId", issueId);
String sql = SqlSpotCheckReportQuery.DELETE_ISSUE_ID.getSql(schema());
jdbcNamed.update(sql, params);
}
/**
* {@inheritDoc}
*/
@Override
public void deleteAllIssueId(int mismatchId) {
MapSqlParameterSource params = new MapSqlParameterSource()
.addValue("mismatchId", mismatchId);
String sql = SqlSpotCheckReportQuery.DELETE_ALL_ISSUE_ID.getSql(schema());
jdbcNamed.update(sql, params);
}
/**
* --- Helper Classes ---
*/
private class MismatchMapper implements RowMapper<DeNormSpotCheckMismatch> {
@Override
public DeNormSpotCheckMismatch<ContentKey> mapRow(ResultSet rs, int rowNum) throws SQLException {
ContentKey key = getKeyFromMap(hstoreStringToMap(rs.getString("key")));
SpotCheckMismatchType type = SpotCheckMismatchType.valueOf(rs.getString("type"));
SpotCheckDataSource dataSource = SpotCheckDataSource.valueOf(rs.getString("datasource"));
DeNormSpotCheckMismatch mismatch = new DeNormSpotCheckMismatch<>(key, type, dataSource);
mismatch.setMismatchId(rs.getInt("mismatch_id"));
mismatch.setReportId(rs.getInt("report_id"));
mismatch.setStatus(SpotCheckMismatchStatus.valueOf(rs.getString("status")));
mismatch.setContentType(SpotCheckContentType.valueOf(rs.getString("content_type")));
mismatch.setReferenceData(rs.getString("reference_data"));
mismatch.setObservedData(rs.getString("observed_data"));
mismatch.setReportDateTime(getLocalDateTimeFromRs(rs, "report_date_time"));
mismatch.setObservedDateTime(getLocalDateTimeFromRs(rs, "observed_date_time"));
mismatch.setNotes(rs.getString("notes"));
mismatch.setIgnoreStatus(SpotCheckMismatchIgnore.valueOf(rs.getString("ignore_status")));
String[] issue_idss = getArrayFromPgRs(rs, "issue_ids");
mismatch.setIssueIds(Sets.newHashSet(issue_idss));
SpotCheckRefType refType = SpotCheckRefType.valueOf(rs.getString("reference_type"));
LocalDateTime refActiveDateTime = getLocalDateTimeFromRs(rs, "reference_active_date_time");
mismatch.setReferenceId(new SpotCheckReferenceId(refType, refActiveDateTime));
return mismatch;
}
}
private class MismatchSummaryHandler implements RowCallbackHandler {
private MismatchSummary summary = new MismatchSummary();
@Override
public void processRow(ResultSet rs) throws SQLException {
SpotCheckContentType contentType = SpotCheckContentType.valueOf(rs.getString("content_type"));
SpotCheckMismatchStatus status = SpotCheckMismatchStatus.valueOf(rs.getString("status"));
int count = rs.getInt("count");
SpotCheckMismatchStatusSummary statusSummary = new SpotCheckMismatchStatusSummary(status, contentType, count);
summary.addStatusSummary(statusSummary);
}
protected MismatchSummary getSummary() {
return summary;
}
}
private MapSqlParameterSource getReportIdParams(SpotCheckReport<ContentKey> report) {
return new MapSqlParameterSource()
.addValue("referenceType", report.getReferenceType().name())
.addValue("reportDateTime", toDate(report.getReportDateTime()))
.addValue("referenceDateTime", toDate(report.getReferenceDateTime()))
.addValue("notes", report.getNotes());
}
}