package gov.nysenate.openleg.dao.sobi; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Range; import gov.nysenate.openleg.dao.base.*; import gov.nysenate.openleg.model.sobi.SobiFile; import gov.nysenate.openleg.model.sobi.SobiFragment; import gov.nysenate.openleg.model.sobi.SobiFragmentType; import gov.nysenate.openleg.util.DateUtils; import org.apache.commons.io.FileExistsException; 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 javax.annotation.PostConstruct; import java.io.File; import java.io.FileNotFoundException; 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.stream.Collectors; import static gov.nysenate.openleg.dao.sobi.SqlSobiQuery.*; import static gov.nysenate.openleg.util.DateUtils.toDate; import static gov.nysenate.openleg.util.FileIOUtils.getSortedFiles; /** * Sobi files are stored in the file system to preserve their original formatting but metadata * for the files are stored in the database. The returned SobiFile instances are constructed * utilizing both data sources. */ @Repository public class SqlFsSobiDao extends SqlBaseDao implements SobiDao { private static final Logger logger = LoggerFactory.getLogger(SqlFsSobiDao.class); /** Directory where new sobi files come in from external sources. */ private File incomingSobiDir; /** Directory where sobi files that have been processed are stored. */ private File archiveSobiDir; @PostConstruct protected void init() { this.incomingSobiDir = new File(environment.getStagingDir(), "sobis"); this.archiveSobiDir = new File(environment.getArchiveDir(), "sobis"); } /** --- Implemented Methods --- */ /** {@inheritDoc} */ @Override public SobiFile getSobiFile(String fileName) { MapSqlParameterSource params = new MapSqlParameterSource("fileNames", Arrays.asList(fileName)); return jdbcNamed.queryForObject( GET_SOBI_FILES_BY_FILE_NAMES.getSql(schema()), params, new SobiFileRowMapper()); } /** {@inheritDoc} */ @Override public Map<String, SobiFile> getSobiFiles(List<String> fileNames) { MapSqlParameterSource params = new MapSqlParameterSource("fileNames", fileNames); Map<String, SobiFile> sobiFileMap = new HashMap<>(); List<SobiFile> sobiList = jdbcNamed.query(GET_SOBI_FILES_BY_FILE_NAMES.getSql(schema()), params, new SobiFileRowMapper()); for (SobiFile sobiFile : sobiList) { sobiFileMap.put(sobiFile.getFileName(), sobiFile); } return sobiFileMap; } /** {@inheritDoc} */ @Override public PaginatedList<SobiFile> getSobiFilesDuring(Range<LocalDateTime> dateTimeRange, SortOrder sortByPubDate, LimitOffset limitOffset) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("startDate", toDate(DateUtils.startOfDateTimeRange(dateTimeRange))); params.addValue("endDate", toDate(DateUtils.endOfDateTimeRange(dateTimeRange))); OrderBy orderBy = new OrderBy("published_date_time", sortByPubDate); PaginatedRowHandler<SobiFile> handler = new PaginatedRowHandler<>(limitOffset, "total_count", new SobiFileRowMapper()); jdbcNamed.query(GET_SOBI_FILES_DURING.getSql(schema(), orderBy, limitOffset), params, handler); return handler.getList(); } /** {@inheritDoc} */ @Override public List<SobiFile> getIncomingSobiFiles(SortOrder sortByFileName, LimitOffset limitOffset) throws IOException { List<File> files = new ArrayList<>(getSortedFiles(this.incomingSobiDir, false, null)); if (sortByFileName.equals(SortOrder.DESC)) { Collections.reverse(files); } files = LimitOffset.limitList(files, limitOffset); List<SobiFile> sobiFiles = new ArrayList<>(); for (File file : files) { sobiFiles.add(new SobiFile(file)); } return sobiFiles; } /** {@inheritDoc} */ @Override public SobiFragment getSobiFragment(String fragmentId) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("fragmentId", fragmentId); return jdbcNamed.queryForObject( GET_SOBI_FRAGMENT_BY_FILE_NAME.getSql(schema()), params, new SobiFragmentRowMapper()); } /** {@inheritDoc} */ @Override public List<SobiFragment> getSobiFragments(SobiFile sobiFile, SortOrder sortById) { MapSqlParameterSource params = new MapSqlParameterSource("sobiFileName", sobiFile.getFileName()); OrderBy orderBy = new OrderBy("fragment_id", sortById); return jdbcNamed.query( GET_SOBI_FRAGMENTS_BY_SOBI_FILE.getSql(schema(), orderBy, LimitOffset.ALL), params, new SobiFragmentRowMapper(sobiFile)); } /** {@inheritDoc} */ @Override public List<SobiFragment> getSobiFragments(SobiFile sobiFile, SobiFragmentType fragmentType, SortOrder sortById) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("sobiFileName", sobiFile.getFileName()); params.addValue("fragmentType", fragmentType.name()); OrderBy orderBy = new OrderBy("fragment_id", sortById); return jdbcNamed.query( GET_SOBI_FRAGMENTS_BY_SOBI_FILE_AND_TYPE.getSql(schema(), orderBy, LimitOffset.ALL), params, new SobiFragmentRowMapper(sobiFile)); } /** {@inheritDoc} */ @Override public List<SobiFragment> getPendingSobiFragments(SortOrder sortById, LimitOffset limOff) { OrderBy orderBy = new OrderBy("fragment_id", sortById); return jdbcNamed.query( GET_PENDING_SOBI_FRAGMENTS.getSql(schema(), orderBy, limOff), new SobiFragmentRowMapper()); } /** {@inheritDoc} */ @Override public List<SobiFragment> getPendingSobiFragments(ImmutableSet<SobiFragmentType> restrict, SortOrder sortById, LimitOffset limOff) { OrderBy orderBy = new OrderBy("fragment_id", sortById); MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("fragmentTypes", restrict.stream().map(Enum::name).collect(Collectors.toSet())); return jdbcNamed.query( GET_PENDING_SOBI_FRAGMENTS_BY_TYPE.getSql(schema(), orderBy, limOff), params, new SobiFragmentRowMapper()); } /** --- Update/Insert Methods --- */ /** {@inheritDoc} */ @Override public void archiveAndUpdateSobiFile(SobiFile sobiFile) throws IOException { File stageFile = sobiFile.getFile(); // Archive the file only if the current one is residing in the incoming sobis directory. if (stageFile.getParentFile().compareTo(incomingSobiDir) == 0) { File archiveFile = getFileInArchiveDir(sobiFile.getFileName(), sobiFile.getPublishedDateTime()); moveFile(stageFile, archiveFile); sobiFile.setFile(archiveFile); sobiFile.setArchived(true); updateSobiFile(sobiFile); } else { throw new FileNotFoundException( "SobiFile " + stageFile + " must be in the incoming sobis directory in order to be archived."); } } /** {@inheritDoc} */ @Override public void updateSobiFile(SobiFile sobiFile) { MapSqlParameterSource params = getSobiFileParams(sobiFile); if (jdbcNamed.update(UPDATE_SOBI_FILE.getSql(schema()), params) == 0) { jdbcNamed.update(INSERT_SOBI_FILE.getSql(schema()), params); } } /** {@inheritDoc} */ @Override public void updateSobiFragment(SobiFragment fragment) { MapSqlParameterSource params = getSobiFragmentParams(fragment); if (jdbcNamed.update(UPDATE_SOBI_FRAGMENT.getSql(schema()), params) == 0) { jdbcNamed.update(INSERT_SOBI_FRAGMENT.getSql(schema()), params); } } /** --- Helper Classes --- */ /** * Maps rows from the sobi file table to SobiFile objects. */ protected class SobiFileRowMapper implements RowMapper<SobiFile> { @Override public SobiFile mapRow(ResultSet rs, int rowNum) throws SQLException { String fileName = rs.getString("file_name"); LocalDateTime publishedDateTime = getLocalDateTimeFromRs(rs, "published_date_time"); boolean archived = rs.getBoolean("archived"); File file = (archived) ? getFileInArchiveDir(fileName, publishedDateTime) : getFileInIncomingDir(fileName); String encoding = rs.getString("encoding"); try { SobiFile sobiFile = new SobiFile(file, encoding); sobiFile.setArchived(archived); sobiFile.setStagedDateTime(getLocalDateTimeFromRs(rs, "staged_date_time")); return sobiFile; } catch (FileNotFoundException ex) { logger.error( "SOBI file " + rs.getString("file_name") + " was not found in the expected location! \n" + "This could be a result of modifications to the sobi file directory that were not synced with " + "the database.", ex); } catch (IOException ex) { logger.error("{}", ex); } return null; } } /** * Maps rows from the sobi fragment table to SobiFragment objects. */ protected class SobiFragmentRowMapper implements RowMapper<SobiFragment> { private String pfx = ""; private Map<String, SobiFile> sobiFileMap = new HashMap<>(); public SobiFragmentRowMapper() { this("", Collections.<SobiFile>emptyList()); } public SobiFragmentRowMapper(SobiFile sobiFile) { this("", Arrays.asList(sobiFile)); } public SobiFragmentRowMapper(String pfx, List<SobiFile> sobiFiles) { this.pfx = pfx; for (SobiFile sobiFile : sobiFiles) { this.sobiFileMap.put(sobiFile.getFileName(), sobiFile); } } @Override public SobiFragment mapRow(ResultSet rs, int rowNum) throws SQLException { String sobiFileName = rs.getString(pfx + "sobi_file_name"); // Passing the sobi file objects in the constructor is a means of caching the objects // so that they don't have to be re-mapped. If not supplied, an extra call will be // made to fetch the sobi file. SobiFile sobiFile = this.sobiFileMap.get(sobiFileName); if (sobiFile == null) { sobiFile = getSobiFile(sobiFileName); } SobiFragmentType type = SobiFragmentType.valueOf(rs.getString(pfx + "fragment_type").toUpperCase()); int sequenceNo = rs.getInt(pfx + "sequence_no"); String text = rs.getString(pfx + "text"); SobiFragment fragment = new SobiFragment(sobiFile, type, text, sequenceNo); fragment.setStagedDateTime(getLocalDateTimeFromRs(rs, "staged_date_time")); fragment.setPendingProcessing(rs.getBoolean("pending_processing")); fragment.setProcessedCount(rs.getInt("processed_count")); fragment.setProcessedDateTime(getLocalDateTimeFromRs(rs, "processed_date_time")); fragment.setManualFix(rs.getBoolean("manual_fix")); fragment.setManualFixNotes(rs.getString("manual_fix_notes")); return fragment; } } /** --- Internal Methods --- */ /** * Get file handle from incoming sobi directory. */ private File getFileInIncomingDir(String fileName) { return new File(this.incomingSobiDir, fileName); } /** * Get file handle from the sobi archive directory. */ private File getFileInArchiveDir(String fileName, LocalDateTime publishedDateTime) { String year = Integer.toString(publishedDateTime.getYear()); File dir = new File(this.archiveSobiDir, year); return new File(dir, fileName); } /** --- Param Source Methods --- */ /** * Returns a MapSqlParameterSource with columns mapped to SobiFile values. */ private MapSqlParameterSource getSobiFileParams(SobiFile sobiFile) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("fileName", sobiFile.getFileName()); params.addValue("encoding", sobiFile.getEncoding()); params.addValue("publishedDateTime", toDate(sobiFile.getPublishedDateTime())); params.addValue("stagedDateTime", toDate(sobiFile.getStagedDateTime())); params.addValue("archived", sobiFile.isArchived()); return params; } /** * Returns a MapSqlParameterSource with columns mapped to SobiFragment values. */ private MapSqlParameterSource getSobiFragmentParams(SobiFragment fragment) { MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("fragmentId", fragment.getFragmentId()); params.addValue("sobiFileName", fragment.getParentSobiFile().getFileName()); params.addValue("publishedDateTime", toDate(fragment.getPublishedDateTime())); params.addValue("fragmentType", fragment.getType().name()); params.addValue("sequenceNo", fragment.getSequenceNo()); // Replace all null characters with empty string. params.addValue("text", fragment.getText().replace('\0', ' ')); params.addValue("processedCount", fragment.getProcessedCount()); params.addValue("processedDateTime", toDate(fragment.getProcessedDateTime())); params.addValue("pendingProcessing", fragment.isPendingProcessing()); params.addValue("manualFix", fragment.isManualFix()); params.addValue("manualFixNotes", fragment.getManualFixNotes()); return params; } }