package gov.nysenate.openleg.dao.entity.committee.data; import gov.nysenate.openleg.dao.base.LimitOffset; import gov.nysenate.openleg.dao.base.OrderBy; import gov.nysenate.openleg.dao.base.SortOrder; import gov.nysenate.openleg.dao.base.SqlBaseDao; import gov.nysenate.openleg.dao.entity.member.data.MemberDao; import gov.nysenate.openleg.model.base.SessionYear; import gov.nysenate.openleg.model.entity.*; import gov.nysenate.openleg.model.sobi.SobiFragment; import gov.nysenate.openleg.model.entity.MemberNotFoundEx; import gov.nysenate.openleg.service.entity.member.data.MemberService; import gov.nysenate.openleg.util.DateUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.RowCallbackHandler; 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.DayOfWeek; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static gov.nysenate.openleg.dao.entity.committee.data.SqlCommitteeQuery.*; @Repository public class SqlCommitteeDao extends SqlBaseDao implements CommitteeDao { public static final Logger logger = LoggerFactory.getLogger(SqlCommitteeDao.class); @Autowired MemberDao memberDao; @Autowired MemberService memberService; /** * {@inheritDoc} * */ @Override public Committee getCommittee(CommitteeId committeeId) throws DataAccessException { logger.debug("Looking up committee " + committeeId); MapSqlParameterSource params = getCommitteeSessionIdParams( new CommitteeSessionId(committeeId, SessionYear.current())); CommitteeRowHandler rowHandler = new CommitteeRowHandler(); jdbcNamed.query(SELECT_COMMITTEE_CURRENT_SQL.getSql(schema()), params, rowHandler); try { return rowHandler.getCommitteeList().get(0); } catch(IndexOutOfBoundsException e){ throw new EmptyResultDataAccessException("Could not find committee in db: " + committeeId, 1, e); } } /** * {@inheritDoc} * */ @Override public Committee getCommittee(CommitteeVersionId committeeVersionId) { logger.debug("Looking up committee " + committeeVersionId); MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); CommitteeRowHandler rowHandler = new CommitteeRowHandler(); jdbcNamed.query(SELECT_COMMITTEE_AT_DATE_SQL.getSql(schema()), params, rowHandler); try { return rowHandler.getCommitteeList().get(0); } catch(IndexOutOfBoundsException e){ throw new EmptyResultDataAccessException("Could not find committee version in db: " + committeeVersionId, 1, e); } } /** * {@inheritDoc} */ @Override public List<CommitteeId> getCommitteeList() { return jdbcNamed.query(SELECT_COMMITTEE_ID.getSql(schema(), new OrderBy("chamber", SortOrder.ASC, "name", SortOrder.ASC), LimitOffset.ALL), new CommitteeIdRowMapper()); } /** * {@inheritDoc} */ @Override public List<SessionYear> getEligibleYears() throws DataAccessException { return jdbcNamed.query(SELECT_SESSION_YEARS.getSql(schema(), new OrderBy("session_year", SortOrder.ASC), LimitOffset.ALL), (rs, num) -> getSessionYearFromRs(rs, "session_year")); } /** * {@inheritDoc} */ @Override public List<CommitteeSessionId> getAllSessionIds() throws DataAccessException { return jdbcNamed.query(SELECT_COMMITTEE_SESSION_IDS.getSql(schema(), new OrderBy("session_year", SortOrder.ASC), LimitOffset.ALL), new CommitteeSessionIdRowMapper()); } /** * {@inheritDoc} * @param committeeSessionId */ @Override public List<Committee> getCommitteeHistory(CommitteeSessionId committeeSessionId) throws DataAccessException { List<Committee> committeeHistory = selectCommitteeVersionHistory(committeeSessionId); if (committeeHistory.isEmpty()) { throw new EmptyResultDataAccessException("No committee history found for given committee id", 1); } return committeeHistory; } /** * {@inheritDoc} */ @Override public void updateCommittee(Committee committee, SobiFragment sobiFragment) { logger.info("Updating committee " + committee.getChamber() + " " + committee.getName()); // Try to create a new committee if (insertCommittee(committee.getId())) { insertCommitteeVersion(committee, sobiFragment); updateCommitteeCurrentVersion(committee.getVersionId()); } else { // if that fails perform updates to an existing committee // delete all committee versions with a creation date after this one deleteFutureCommitteeVersions(committee.getVersionId()); try { Committee existingCommittee = getCommittee(committee.getVersionId()); updateExistingCommittee(committee, existingCommittee, sobiFragment); } catch (EmptyResultDataAccessException ex) { // No committee version exists for this session // Insert this committee as the first version of the session insertCommitteeVersion(committee, sobiFragment); updateCommitteeCurrentVersion(committee.getVersionId()); } } } /** * {@inheritDoc} */ @Override public void deleteCommittee(CommitteeId committeeId) { logger.info("Deleting all records for " + committeeId); MapSqlParameterSource params = getCommitteeIdParams(committeeId); jdbcNamed.update(DELETE_COMMITTEE.getSql(schema()),params); } /** --- Private Methods --- */ /** * Tries to insert a new committee into the database from the given parameter * @param committeeId * @return true if a new committee was created, false if the committee already exists */ private boolean insertCommittee(CommitteeId committeeId) { logger.debug("Creating new committee " + committeeId); // Create the committee MapSqlParameterSource params = getCommitteeIdParams(committeeId); try { jdbcNamed.update(INSERT_COMMITTEE.getSql(schema()), params); logger.info("Created new committee " + committeeId); } catch(DuplicateKeyException e) { logger.debug("\tCommittee " + committeeId + " already exists"); return false; } return true; } /** * Creates a record for a new version of a committee * @param committee */ private void insertCommitteeVersion(Committee committee, SobiFragment sobiFragment){ logger.debug("Inserting new version of " + committee.getVersionId()); MapSqlParameterSource params = getCommitteeVersionParams(committee); addLastFragmentParam(sobiFragment, params); jdbcNamed.update(INSERT_COMMITTEE_VERSION.getSql(schema()), params); insertCommitteeMembers(committee); } /** * Inserts the committee members for a particular version of a committee * @param committee */ private void insertCommitteeMembers(Committee committee){ for (CommitteeMember committeeMember : committee.getMembers()) { MapSqlParameterSource params = getCommitteeMemberParams(committeeMember, committee.getVersionId()); jdbcNamed.update(INSERT_COMMITTEE_MEMBER.getSql(schema()), params); } } /** * Gets committee versions fo a given comittee, within the specified dateRange * @param committeeSessionId * @return */ private List<Committee> selectCommitteeVersionHistory(CommitteeSessionId committeeSessionId){ MapSqlParameterSource params = getCommitteeSessionIdParams(committeeSessionId); CommitteeRowHandler rowHandler = new CommitteeRowHandler(); jdbcNamed.query(SELECT_COMMITTEE_VERSION_HISTORY.getSql( schema(), new OrderBy("created", SortOrder.DESC), LimitOffset.ALL), params, rowHandler); return rowHandler.getCommitteeList(); } /** * Retrieves the committee version with the next lowest created date from the given committee version * @param committeeVersionId * @return */ private Committee selectPreviousCommittee(CommitteeVersionId committeeVersionId){ MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); CommitteeRowHandler rowHandler = new CommitteeRowHandler(); jdbcNamed.query(SELECT_PREVIOUS_COMMITTEE_VERSION.getSql(schema()), params, rowHandler); try { return rowHandler.getCommitteeList().get(0); } catch(IndexOutOfBoundsException e){ return null; } } /** * Modifies the record of an existing committee to create a new version of the committee * @param committee * @param sobiFragment */ private void updateExistingCommittee(Committee committee, Committee existingCommittee, SobiFragment sobiFragment){ logger.debug("Updating committee " + committee.getChamber() + " " + committee.getName() + " published on " + committee.getPublishedDateTime()); if (!committee.membersEquals(existingCommittee)) { // if there has been a change in membership logger.debug("\tMember discrepancy detected.. creating new version"); // replace existing committee if they share the same creation date if (committee.getPublishedDateTime().equals(existingCommittee.getPublishedDateTime())) { deleteCommitteeVersion(existingCommittee.getVersionId()); Committee previousCommittee = selectPreviousCommittee(existingCommittee.getVersionId()); if (previousCommittee!=null && committee.membersEquals(previousCommittee)) { // Merge with previous committee if same membership mergeCommittees(previousCommittee, committee, sobiFragment); committee = previousCommittee; } else { // Create a new version of the committee insertCommitteeVersion(committee, sobiFragment); } } else { // Create a new version of the committee and update reformed for existing committee insertCommitteeVersion(committee, sobiFragment); existingCommittee.setReformed(committee.getPublishedDateTime()); updateCommitteeReformed(existingCommittee, sobiFragment); } // Update references updateCommitteeCurrentVersion(committee.getVersionId()); } // If there has been a change in meeting protocol else if (!committee.meetingEquals(existingCommittee)) { logger.debug("\tMeeting discrepancy detected.. updating version"); // Update the meeting information for the existing version existingCommittee.updateMeetingInfo(committee); updateCommitteeMeetingInfo(existingCommittee, sobiFragment); } else { logger.debug("\tNo changes detected, no updates performed"); } } /** * Modifies the record of a given committee version update data relating to meetings * @param committee */ private void updateCommitteeMeetingInfo(Committee committee, SobiFragment sobiFragment){ MapSqlParameterSource params = getCommitteeVersionParams(committee); addLastFragmentParam(sobiFragment, params); jdbcNamed.update(UPDATE_COMMITTEE_MEETING_INFO.getSql(schema()), params); } /** * Sets the reformed date for a given commitee version * @param committee */ private void updateCommitteeReformed(Committee committee, SobiFragment sobiFragment){ logger.debug("updating reformed date for" + committee.getVersionId() + " to " + committee.getReformed()); MapSqlParameterSource params = getCommitteeVersionIdParams(committee.getVersionId()); addLastFragmentParam(sobiFragment, params); params.addValue("reformed", DateUtils.toDate(committee.getReformed())); jdbcNamed.update(UPDATE_COMMITTEE_VERSION_REFORMED.getSql(schema()), params); } /** * Updates the current version of a committee record to the version specified by the given committee * @param committeeVersionId */ private void updateCommitteeCurrentVersion(CommitteeVersionId committeeVersionId) { logger.debug("updating current version of " + committeeVersionId); MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); jdbcNamed.update(UPDATE_COMMITTEE_CURRENT_VERSION.getSql(schema()), params); } /** * Given two committees, merges records for the second committee into the first creating a single version record * @param first * @param second */ private void mergeCommittees(Committee first, Committee second, SobiFragment sobiFragment) { first.updateMeetingInfo(second); first.setReformed(second.getReformed()); deleteCommitteeVersion(second.getVersionId()); updateCommitteeMeetingInfo(first, sobiFragment); updateCommitteeReformed(first, sobiFragment); if(second.isCurrent()){ updateCommitteeCurrentVersion(first.getVersionId()); } } /** * Removes all committee versions for the given committee that occur after the given created date * @param committeeVersionId */ protected void deleteFutureCommitteeVersions(CommitteeVersionId committeeVersionId) { MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); jdbcNamed.update(DELETE_COMMITTEE_VERSION_FUTURE.getSql(schema()), params); } /** * Removes the all entries for a given committee version * @param committeeVersionId */ private void deleteCommitteeVersion(CommitteeVersionId committeeVersionId) { deleteCommitteeMembers(committeeVersionId); MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); jdbcNamed.update(DELETE_COMMITTEE_VERSION.getSql(schema()), params); } /** * Removes all committee member records for a given committee version * @param committeeVersionId */ private void deleteCommitteeMembers(CommitteeVersionId committeeVersionId) { MapSqlParameterSource params = getCommitteeVersionIdParams(committeeVersionId); jdbcNamed.update(DELETE_COMMITTEE_MEMBERS.getSql(schema()), params); } /** --- Row Mappers --- */ protected class CommitteeIdRowMapper implements RowMapper<CommitteeId> { @Override public CommitteeId mapRow(ResultSet rs, int rowNum) throws SQLException { return new CommitteeId( Chamber.getValue(rs.getString("chamber")), rs.getString("name") ); } } protected class CommitteeSessionIdRowMapper implements RowMapper<CommitteeSessionId> { @Override public CommitteeSessionId mapRow(ResultSet rs, int rowNum) throws SQLException { return new CommitteeSessionId( Chamber.getValue(rs.getString("chamber")), rs.getString("committee_name"), getSessionYearFromRs(rs, "session_year") ); } } protected class CommitteeVersionIdRowMapper implements RowMapper<CommitteeVersionId> { protected CommitteeSessionIdRowMapper sessionIdRowMapper = new CommitteeSessionIdRowMapper(); @Override public CommitteeVersionId mapRow(ResultSet rs, int rowNum) throws SQLException { return new CommitteeVersionId( sessionIdRowMapper.mapRow(rs, rowNum), getLocalDateTimeFromRs(rs, "created") ); } } protected class CommitteeRowMapper implements RowMapper<Committee> { @Override public Committee mapRow(ResultSet rs, int i) throws SQLException { Committee committee = new Committee(); committee.setName(rs.getString("committee_name")); committee.setChamber(Chamber.getValue(rs.getString("chamber"))); committee.setPublishedDateTime(getLocalDateTimeFromRs(rs, "created")); LocalDateTime reformed = getLocalDateTimeFromRs(rs, "reformed"); committee.setReformed(reformed.isBefore(LocalDateTime.now()) ? reformed : null); committee.setLocation(rs.getString("location")); committee.setMeetDay(StringUtils.isNotEmpty(rs.getString("meetday")) ? DayOfWeek.valueOf(rs.getString("meetday").toUpperCase()) : null); committee.setMeetTime(rs.getTime("meettime") != null ? rs.getTime("meettime").toLocalTime() : null); committee.setMeetAltWeek(rs.getBoolean("meetaltweek")); committee.setMeetAltWeekText(rs.getString("meetaltweektext")); committee.setSession(getSessionYearFromRs(rs, "session_year")); return committee; } } protected class CommitteeMemberRowMapper implements RowMapper<CommitteeMember> { @Override public CommitteeMember mapRow(ResultSet rs, int i) throws SQLException { CommitteeMember committeeMember = new CommitteeMember(); committeeMember.setSequenceNo(rs.getInt("sequence_no")); int sessionMemberId = rs.getInt("session_member_id"); try { committeeMember.setMember(memberService.getMemberBySessionId(sessionMemberId)); } catch (MemberNotFoundEx memberNotFoundEx) { logger.error(String.valueOf(memberNotFoundEx)); } committeeMember.setTitle(CommitteeMemberTitle.valueOfSqlEnum(rs.getString("title"))); committeeMember.setMajority(rs.getBoolean("majority")); return committeeMember; } } protected class CommitteeRowHandler implements RowCallbackHandler { protected CommitteeRowMapper committeeRowMapper = new CommitteeRowMapper(); protected CommitteeMemberRowMapper committeeMemberRowMapper = new CommitteeMemberRowMapper(); protected CommitteeVersionIdRowMapper versionIdRowMapper = new CommitteeVersionIdRowMapper(); protected Map<CommitteeVersionId, Committee> committeeMap; public CommitteeRowHandler() { committeeMap = new LinkedHashMap<>(); } @Override public void processRow(ResultSet rs) throws SQLException { CommitteeVersionId versionId = versionIdRowMapper.mapRow(rs, rs.getRow()); if (!committeeMap.containsKey(versionId)) { committeeMap.put(versionId, committeeRowMapper.mapRow(rs, rs.getRow())); } committeeMap.get(versionId).addMember(committeeMemberRowMapper.mapRow(rs, rs.getRow())); } public Map<CommitteeVersionId, Committee> getCommitteeMap() { return committeeMap; } public List<Committee> getCommitteeList() { return new ArrayList<>(committeeMap.values()); } } /** --- Param Source Methods --- */ private MapSqlParameterSource getCommitteeIdParams(CommitteeId cid) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("committeeName", cid.getName()); params.addValue("chamber", cid.getChamber().asSqlEnum()); return params; } private MapSqlParameterSource getCommitteeSessionIdParams(CommitteeSessionId csid) { MapSqlParameterSource params = getCommitteeIdParams(csid); params.addValue("sessionYear", csid.getSession().getYear()); return params; } private MapSqlParameterSource getCommitteeVersionIdParams(CommitteeVersionId cvid) { MapSqlParameterSource params = getCommitteeSessionIdParams(cvid); params.addValue("referenceDate", DateUtils.toDate(cvid.getReferenceDate())); return params; } private MapSqlParameterSource getCommitteeVersionParams(Committee committee) { MapSqlParameterSource params = getCommitteeVersionIdParams(committee.getVersionId()); params.addValue("location", committee.getLocation()); params.addValue("meetday", committee.getMeetDay() != null ? committee.getMeetDay().toString() : null); params.addValue("meettime", DateUtils.toTime(committee.getMeetTime())); params.addValue("meetaltweek", committee.isMeetAltWeek()); params.addValue("meetaltweektext", committee.getMeetAltWeekText()); return params; } private MapSqlParameterSource getCommitteeMemberParams(CommitteeMember committeeMember, CommitteeVersionId cvid) { MapSqlParameterSource params = getCommitteeVersionIdParams(cvid); params.addValue("session_member_id", committeeMember.getMember().getSessionMemberId()); params.addValue("sequence_no", committeeMember.getSequenceNo()); params.addValue("title", committeeMember.getTitle().asSqlEnum()); params.addValue("majority", committeeMember.isMajority()); return params; } }