package gov.nysenate.openleg.processor;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import gov.nysenate.openleg.config.Environment;
import gov.nysenate.openleg.model.process.*;
import gov.nysenate.openleg.processor.base.ProcessService;
import gov.nysenate.openleg.processor.hearing.PublicHearingProcessService;
import gov.nysenate.openleg.processor.law.LawProcessService;
import gov.nysenate.openleg.processor.sobi.SobiProcessService;
import gov.nysenate.openleg.processor.transcript.TranscriptProcessService;
import gov.nysenate.openleg.service.process.DataProcessLogService;
import gov.nysenate.openleg.service.spotcheck.agenda.AgendaSpotcheckProcessService;
import gov.nysenate.openleg.service.spotcheck.base.BaseSpotcheckProcessService;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Process all the things.
*/
@Service
public class DataProcessor
{
private static final Logger logger = LoggerFactory.getLogger(DataProcessor.class);
@Autowired private Environment env;
@Autowired private EventBus eventBus;
@Autowired private DataProcessLogService processLogService;
@Autowired private SobiProcessService sobiProcessService;
@Autowired private TranscriptProcessService transcriptProcessService;
@Autowired private PublicHearingProcessService publicHearingProcessService;
@Autowired private LawProcessService lawProcessService;
@Autowired private List<BaseSpotcheckProcessService> spotcheckProcessServices;
private List<ProcessService> processServices;
/** Hold a reference to the current data process run instance for event-based logging purposes. */
private DataProcessRun currentRun;
@PostConstruct
public void init() {
eventBus.register(this);
processServices = ImmutableList.<ProcessService>builder()
.add(sobiProcessService)
.add(transcriptProcessService)
.add(publicHearingProcessService)
.add(lawProcessService)
.addAll(spotcheckProcessServices)
.build();
}
/** --- Main Methods --- */
/**
* Simple entry point to process new data for all supported data types.
* @throws Exception
*/
public synchronized DataProcessRun run(String invoker) throws Exception {
if (env.isProcessingEnabled()) {
logger.info("Starting data processor...");
currentRun = processLogService.startNewRun(LocalDateTime.now(), invoker);
try {
collate();
ingest();
}
catch (Exception ex) {
eventBus.post(new DataProcessErrorEvent("Unexpected Processing Error", ex, currentRun.getProcessId()));
logger.error("Unexpected Processing Error:\n{}", ExceptionUtils.getStackTrace(ex));
}
processLogService.finishRun(currentRun);
DataProcessRun finishedRun = currentRun;
currentRun = null;
logger.info("Exiting data processor.");
return finishedRun;
}
else {
logger.debug("Data processing is disabled!");
return null;
}
}
/**
* If scheduled processing is enabled, the #run method will be invoked according to the
* configured cron value.
*/
@Scheduled(cron = "${scheduler.process.cron}")
public synchronized void scheduledRun() {
if (env.isProcessingScheduled()) {
try {
run("Scheduler");
}
// Scheduled methods cannot let checked exceptions through
catch (Exception ex) {
logger.error("Caught exception while processing data\n{}", ExceptionUtils.getStackTrace(ex));
}
}
}
/** --- Event Handlers --- */
@Subscribe
public void handleDataProcessErrorEvent(DataProcessErrorEvent ev) {
if (currentRun != null) {
currentRun.addException(ev.getMessage(), ev.getEx());
}
}
@Subscribe
public void handleDataProcessUnitEvent(DataProcessUnitEvent ev) {
if (currentRun != null) {
DataProcessUnit unit = ev.getUnit();
processLogService.addUnit(currentRun.getProcessId(), unit);
if (!unit.getErrors().isEmpty()) {
eventBus.post(new DataProcessWarnEvent(currentRun.getProcessId(), unit));
}
}
}
/** --- Processing methods --- */
public synchronized void collate() {
logger.debug("Begin collating data");
Map<String, Integer> collatedCounts = new LinkedHashMap<>();
for (ProcessService processor : processServices) {
if (env.isProcessingEnabled()) {
int collatedCount = processor.collate();
if (collatedCount > 0) {
collatedCounts.put(processor.getCollateType(), collatedCount);
}
}
}
if (collatedCounts.size() > 0) {
logger.debug("Completed collations:");
logCounts(collatedCounts);
}
else {
logger.info("Nothing to collate");
}
}
public synchronized void ingest() throws IOException {
logger.debug("Begin ingesting data");
Map<String, Integer> ingestedCounts = new LinkedHashMap<>();
for (ProcessService processor : processServices) {
if (env.isProcessingEnabled()) {
int ingestedCount = processor.ingest();
if (ingestedCount > 0) {
ingestedCounts.put(processor.getIngestType(), ingestedCount);
}
}
}
if (ingestedCounts.size() > 0) {
logger.debug("Completed ingestion:");
logCounts(ingestedCounts);
}
else {
logger.info("Nothing to ingest");
}
}
public Optional<DataProcessRun> getCurrentRun() {
return Optional.of(currentRun);
}
private void logCounts(Map<String, Integer> counts) {
counts.forEach((type, count) -> logger.info("{}: {}", type, count));
}
}