package gov.nysenate.openleg.dao.bill.data; import com.google.common.collect.Range; import gov.nysenate.openleg.dao.base.*; import gov.nysenate.openleg.model.bill.BaseBillId; import gov.nysenate.openleg.model.bill.BillUpdateField; import gov.nysenate.openleg.model.updates.UpdateContentType; import gov.nysenate.openleg.model.updates.UpdateDigest; import gov.nysenate.openleg.model.updates.UpdateToken; import gov.nysenate.openleg.model.updates.UpdateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.stereotype.Repository; import java.sql.ResultSet; import java.sql.SQLException; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; import static gov.nysenate.openleg.dao.bill.data.SqlBillUpdatesQuery.SELECT_BILL_UPDATE_DIGESTS; import static gov.nysenate.openleg.dao.bill.data.SqlBillUpdatesQuery.SELECT_BILL_UPDATE_TOKENS; import static gov.nysenate.openleg.dao.bill.data.SqlBillUpdatesQuery.SELECT_UPDATE_DIGESTS_FOR_SPECIFIC_BILL; import static gov.nysenate.openleg.model.bill.BillUpdateField.*; @Repository public class SqlBillUpdatesDao extends SqlBaseDao implements BillUpdatesDao { private static final Logger logger = LoggerFactory.getLogger(SqlBillUpdatesDao.class); /** * Simple object to hold table names and column values needed to filter by update types. */ private static class BillUpdateTable { public SqlTable table; public List<String> columns; public BillUpdateTable(SqlTable table, String... columns) { this.table = table; this.columns = Arrays.asList(columns); } } private final static Map<BillUpdateField, BillUpdateTable> updateMappings = new HashMap<>(); static { updateMappings.put(PUBLISHED_BILL, new BillUpdateTable(SqlTable.BILL, "published_date_time")); updateMappings.put(ACT_CLAUSE, new BillUpdateTable(SqlTable.BILL_AMENDMENT, "act_clause")); updateMappings.put(ACTION, new BillUpdateTable(SqlTable.BILL_AMENDMENT_ACTION)); updateMappings.put(ACTIVE_VERSION, new BillUpdateTable(SqlTable.BILL, "active_version")); updateMappings.put(APPROVAL, new BillUpdateTable(SqlTable.BILL_APPROVAL)); updateMappings.put(COSPONSOR, new BillUpdateTable(SqlTable.BILL_AMENDMENT_COSPONSOR)); updateMappings.put(FULLTEXT, new BillUpdateTable(SqlTable.BILL_AMENDMENT, "full_text")); updateMappings.put(LAW, new BillUpdateTable(SqlTable.BILL_AMENDMENT, "law_code", "law_section")); updateMappings.put(MEMO, new BillUpdateTable(SqlTable.BILL_AMENDMENT, "sponsor_memo")); updateMappings.put(MULTISPONSOR, new BillUpdateTable(SqlTable.BILL_AMENDMENT_MULTISPONSOR)); updateMappings.put(SPONSOR, new BillUpdateTable(SqlTable.BILL_SPONSOR)); updateMappings.put(STATUS, new BillUpdateTable(SqlTable.BILL, "status", "status_date", "bill_cal_no", "committee_name", "committee_chamber")); updateMappings.put(STATUS_CODE, new BillUpdateTable(SqlTable.BILL, "status")); updateMappings.put(SUMMARY, new BillUpdateTable(SqlTable.BILL, "summary")); updateMappings.put(TITLE, new BillUpdateTable(SqlTable.BILL, "title")); updateMappings.put(VETO, new BillUpdateTable(SqlTable.BILL_VETO)); updateMappings.put(VOTE, new BillUpdateTable(SqlTable.BILL_AMENDMENT_VOTE_INFO)); } /** {@inheritDoc} */ @Override public PaginatedList<UpdateToken<BaseBillId>> getUpdates(Range<LocalDateTime> dateTimeRange, UpdateType type, BillUpdateField filter, SortOrder dateOrder, LimitOffset limOff) { MapSqlParameterSource params = new MapSqlParameterSource(); addDateTimeRangeParams(params, dateTimeRange); String sqlQuery = getSqlQuery(false, null, type, filter, dateOrder, limOff); PaginatedRowHandler<UpdateToken<BaseBillId>> handler = new PaginatedRowHandler<>(limOff, "total_updated", getBillUpdateTokenFromRs); jdbcNamed.query(sqlQuery, params, handler); return handler.getList(); } /** {@inheritDoc} */ @Override public PaginatedList<UpdateDigest<BaseBillId>> getDetailedUpdates(Range<LocalDateTime> dateTimeRange, UpdateType type, BillUpdateField filter, SortOrder dateOrder, LimitOffset limOff) { MapSqlParameterSource params = new MapSqlParameterSource(); addDateTimeRangeParams(params, dateTimeRange); String sqlQuery = getSqlQuery(true, null, type, filter, dateOrder, limOff); PaginatedRowHandler<UpdateDigest<BaseBillId>> handler = new PaginatedRowHandler<>(limOff, "total_updated", new BillUpdateDigestMapper(filter)); jdbcNamed.query(sqlQuery, params, handler); return handler.getList(); } /** {@inheritDoc} */ @Override public PaginatedList<UpdateDigest<BaseBillId>> getDetailedUpdatesForBill( BaseBillId billId, Range<LocalDateTime> dateTimeRange, UpdateType type, BillUpdateField filter, SortOrder dateOrder, LimitOffset limOff) { MapSqlParameterSource params = new MapSqlParameterSource("printNo", billId.getBasePrintNo()) .addValue("session", billId.getSession().getYear()); addDateTimeRangeParams(params, dateTimeRange); String sqlQuery = getSqlQuery(true, billId, type, filter, dateOrder, limOff); PaginatedRowHandler<UpdateDigest<BaseBillId>> handler = new PaginatedRowHandler<>(limOff, "total_updated", new BillUpdateDigestMapper(filter)); jdbcNamed.query(sqlQuery, params, handler); return handler.getList(); } /** --- Internal --- */ /** * Generates the appropriate sql query based on the args, to remove code duplication. */ private String getSqlQuery(boolean detail, BaseBillId billId, UpdateType updateType, BillUpdateField fieldFilter, SortOrder sortOrder, LimitOffset limOff) { String dateColumn = getDateColumnForUpdateType(updateType); OrderBy orderBy = getOrderByForUpdateType(updateType, sortOrder); String sqlQuery; if (billId != null) { sqlQuery = SELECT_UPDATE_DIGESTS_FOR_SPECIFIC_BILL.getSql(schema(), orderBy, limOff); } else { sqlQuery = (detail) ? SELECT_BILL_UPDATE_DIGESTS.getSql(schema(), orderBy, limOff) : SELECT_BILL_UPDATE_TOKENS.getSql(schema(), orderBy, limOff); } sqlQuery = queryReplace(sqlQuery, "dateColumn", dateColumn); sqlQuery = queryReplace(sqlQuery, "updateFieldFilter", getUpdateFieldFilter(fieldFilter)); return sqlQuery; } /** * Generates a sql fragment to be used in the 'where clause' based on the BillUpdateField. * E.g. given BillUpdateField.STATUS, it will return something like "table_name = 'bill' AND defined(data, 'status')" * which can be plugged into the sql query. */ private String getUpdateFieldFilter(BillUpdateField field) { if (field != null && updateMappings.containsKey(field)) { BillUpdateTable updateTable = updateMappings.get(field); StringBuilder whereClause = new StringBuilder(); whereClause.append("table_name = '").append(updateTable.table.getTableName()).append("'"); if (!updateTable.columns.isEmpty()) { List<String> existKeys = updateTable.columns.stream() .map(column -> "exist(data, '" + column + "')") .collect(Collectors.toList()); whereClause.append(" AND ").append("( ").append(String.join(" OR ", existKeys)).append(") "); } return whereClause.toString(); } // Return an always true conditional if no filters are specified. return "1 = 1"; } /** --- Row Mappers -- */ private static final RowMapper<UpdateToken<BaseBillId>> getBillUpdateTokenFromRs = (rs, rowNum) -> new UpdateToken<>(new BaseBillId(rs.getString("bill_print_no"), rs.getInt("bill_session_year")), UpdateContentType.BILL, rs.getString("last_fragment_id"), getLocalDateTimeFromRs(rs, "last_published_date_time"), getLocalDateTimeFromRs(rs, "last_processed_date_time")); private static class BillUpdateDigestMapper implements RowMapper<UpdateDigest<BaseBillId>> { private BillUpdateField fieldFilter; public BillUpdateDigestMapper(BillUpdateField fieldFilter) { this.fieldFilter = fieldFilter; } @Override public UpdateDigest<BaseBillId> mapRow(ResultSet rs, int rowNum) throws SQLException { // Construct the digest model UpdateDigest<BaseBillId> digest = new UpdateDigest<>(getBillUpdateTokenFromRs.mapRow(rs, rowNum)); // Filter out the data values depending on the filter Map<String, String> data = getHstoreMap(rs, "data"); if (fieldFilter != null) { BillUpdateTable updateTable = updateMappings.get(fieldFilter); if (updateTable != null && !updateTable.columns.isEmpty()) { Set<String> columnSet = new HashSet<>(updateTable.columns); data.keySet().retainAll( data.keySet().stream().filter(col -> columnSet.contains(col)).collect(Collectors.toSet())); } } digest.setAction(rs.getString("action")); digest.setTable(rs.getString("table_name")); digest.setFields(data); return digest; } } }