package jeffaschenk.commons.system.internal.file.services.dropzone;
import jeffaschenk.commons.system.internal.file.services.GlobalConstants;
import jeffaschenk.commons.system.internal.file.services.extract.ExtractProcessingService;
import jeffaschenk.commons.system.internal.file.services.extract.ExtractProcessingTask;
import jeffaschenk.commons.system.internal.scheduling.LocalSchedulingService;
import jeffaschenk.commons.system.internal.scheduling.events.LifeCycleServiceStateType;
import jeffaschenk.commons.system.internal.scheduling.events.LifeCycleServiceType;
import jeffaschenk.commons.system.internal.scheduling.events.LifeCycleServicesEvent;
import jeffaschenk.commons.touchpoint.model.transitory.WatcherStatistic;
import jeffaschenk.commons.types.StatusOutputType;
import jeffaschenk.commons.types.WatcherStatisticType;
import jeffaschenk.commons.util.StringUtils;
import jeffaschenk.commons.util.TimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.nio.file.*;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import static java.nio.file.StandardWatchEventKinds.*;
/**
* Zone Watcher Processing Service Implementation
*
* @author jeffaschenk@gmail.com
*/
@Service("zoneWatcherProcessingService")
public class ZoneWatcherProcessingServiceImpl implements ZoneWatcherProcessingService,
ApplicationContextAware, ApplicationEventPublisherAware, GlobalConstants {
/**
* Logging
*/
private final static Logger logger = LoggerFactory.getLogger(ZoneWatcherProcessingServiceImpl.class);
/**
* Initialization Indicator.
*/
private boolean initialized = false;
/**
* Global Last Time Extract Processing triggered.
*/
private long lastTimeExtractProcessingTriggered = -1;
/**
* Global AutoWired Properties
*/
@Value("#{systemEnvironmentProperties['drop.zone.os.file.directory']}")
private String dropZoneFileDirectoryName;
@Value("#{systemEnvironmentProperties['drop.zone.magic.number.of.files.trigger']}")
private String magicDropZoneNumberOfFilesTriggerString;
private int magicDropZoneNumberOfFilesTrigger;
@Value("#{systemEnvironmentProperties['drop.zone.magic.filename.prefix.trigger']}")
private String magicDropZoneFilenamePrefixTrigger;
/**
* Extract Service
*/
@Autowired
private ExtractProcessingService extractProcessingService;
/**
* Task Executor
*/
@Autowired
private ThreadPoolTaskExecutor taskExecutor;
/**
* Scheduler Service
*/
@Autowired
private LocalSchedulingService schedulerService;
/**
* Zone Watcher Thread Object
*/
private ZoneWatcherTask zoneWatcherTask;
/**
* Singleton Globals for Watch Service
*/
private WatchService watcher;
private Map<WatchKey, WatcherStatistic> watcherStatistics = new HashMap<WatchKey, WatcherStatistic>();
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
/**
* Spring Application Context,
* used to obtain access to Resources on
* Classpath.
*/
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* Spring Application Event Publisher
*/
private ApplicationEventPublisher publisher;
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
/**
* Initialize the Service Provider Interface
*/
@PostConstruct
public synchronized void initialize() {
logger.info("Zone Watcher Service Provider Facility is Initializing.");
this.magicDropZoneNumberOfFilesTrigger =
(StringUtils.isEmpty(this.magicDropZoneNumberOfFilesTriggerString)?
-1:Integer.parseInt(this.magicDropZoneNumberOfFilesTriggerString));
try {
if (StringUtils.isEmpty(this.dropZoneFileDirectoryName)) {
logger.info("No Zone Watcher Directory specified, Service Provider Facility is not Available.");
this.initialized = false;
return;
}
this.watcher = FileSystems.getDefault().newWatchService();
// Register the Drop Zone Directory to Monitor
this.registerDirectoryToWatch(WatcherStatisticType.DROP_ZONE, Paths.get(dropZoneFileDirectoryName));
// Now Start the Actual Service Worker Thread to perform the Watching Facility.
zoneWatcherTask = new ZoneWatcherTask();
this.taskExecutor.execute(zoneWatcherTask);
this.initialized = true;
logger.info("Zone Watcher Service Provider Facility is Ready and Available.");
} catch (IOException ioe) {
logger.error("Issue Establishing File System Watch Service, unable to provide Zone Watcher Services!", ioe);
}
}
/**
* Destroy Service
* Invoked during Termination of the Spring Container.
*/
@PreDestroy
public synchronized void destroy() {
try {
if (this.initialized) {
if (this.zoneWatcherTask != null) {
this.zoneWatcherTask.setStopProcess(true);
this.watcherStatistics.clear();
this.watcher.close();
}
logger.info("Zone Watcher Service Provider Facility has been Shutdown.");
}
} catch (IOException ioe) {
logger.error("Issue Finalizing File System Watch Service!", ioe);
}
}
/**
* Provide status of Zone Watcher Processing Service.
*/
@Override
public String status(StatusOutputType statusOutputType) {
StringBuilder sb = new StringBuilder();
// TODO
return sb.toString();
}
/**
* Provide Running Status
*/
public boolean isRunning() {
return (this.initialized);
}
/**
* Register the Specified directory with the WatchService
*/
private void registerDirectoryToWatch(WatcherStatisticType watcherStatisticType, Path dir) throws IOException {
WatchKey key = dir.register(this.watcher, ENTRY_CREATE, ENTRY_DELETE);
this.watcherStatistics.put(key, new WatcherStatistic(watcherStatisticType, dir));
}
/**
* Zone Watcher Task Thread.
*/
private class ZoneWatcherTask implements Runnable {
private ZoneWatcherTask() {
}
private boolean stopProcess = false;
protected boolean isStopProcess() {
return stopProcess;
}
protected void setStopProcess(boolean stopProcess) {
this.stopProcess = stopProcess;
}
/**
* Perform Zone Watcher Task.
*/
public void run() {
try {
// Avoid a Spring Bug, which would cause a Hang
// when we publish an event too quickly at startup.
Thread.sleep(10*1000);
} catch (InterruptedException x) {
// NoOp
}
// ************************************
// Publish a Life Cycle Services Event
LifeCycleServicesEvent event = new LifeCycleServicesEvent(this, LifeCycleServiceType.ZONE,
LifeCycleServiceStateType.BEGIN, TimeUtils.now());
publisher.publishEvent(event);
// ************************************
// Begin Zone Thread Loop.
while (true) {
if (isStopProcess()) {
break;
}
// *****************************************
// Wait for key to be signalled based upon
// Registered Events to appear
WatchKey watchKey;
try {
watchKey = watcher.take();
} catch (InterruptedException x) {
if (isStopProcess()) {
break;
}
return;
}
// Find our Statistic Bucket Reference..
WatcherStatistic watcherStatistic = watcherStatistics.get(watchKey);
if (watcherStatistic == null) {
continue;
}
// ****************************************
// Poll for Events
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
if (isStopProcess()) {
break;
}
WatchEvent.Kind kind = watchEvent.kind();
// We have Overflowed the event Stack,
// for now Ignore.
if (kind == OVERFLOW) {
logger.warn("Overflow has occurred on Watcher Event Stack, some Events may have been Lost and not Processed!");
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(watchEvent);
Path name = ev.context();
Path child = watcherStatistic.getWatcherPath().resolve(name);
// Log the Event
if (logger.isWarnEnabled()) {
logger.warn("Zone Watcher Event:[" + kind.name() + "], File:[" + child + "]");
}
// Update Statistic Buckets
if (kind == ENTRY_CREATE) {
watcherStatistic.incrementCycleFileCreations();
watcherStatistic.incrementTotalFileCreations();
// If we are monitoring for a specific file to be created
// for triggering an Extract run.
if ((StringUtils.isNotEmpty(magicDropZoneFilenamePrefixTrigger)) &&
(child.toFile().getName().startsWith(magicDropZoneFilenamePrefixTrigger))) {
watcherStatistic.setCycleFilenamePrefixTriggerPresent(true);
// Remove the Trigger File.
child.toFile().delete();
}
} else if (kind == ENTRY_DELETE) {
watcherStatistic.incrementCycleFileDeletions();
watcherStatistic.incrementTotalFileDeletions();
}
// ********************************************
// Review the Statistic Bucket just used and
// determine if any Tasks should be auto-started
// based upon new Files being present.
//
if ((watcherStatistic.getWatcherStatisticType() == WatcherStatisticType.DROP_ZONE) &&
(!extractProcessingService.isRunning())) {
// Is our Magic Number Processing Used?
if ((magicDropZoneNumberOfFilesTrigger > 0) &&
(watcherStatistic.getCycleFileCreations() >= magicDropZoneNumberOfFilesTrigger)) {
triggerProcess(watcherStatistic, "->DropZone Number of Files Trigger Detected:[" + magicDropZoneNumberOfFilesTrigger + "]");
// Clear Cycle Statistics for this Object.
watcherStatistic.setCycleFileCreations(0);
watcherStatistic.setCycleFileDeletions(0);
// Is our Magic FileName Prefix Specified?
} else if (StringUtils.isNotEmpty(magicDropZoneFilenamePrefixTrigger) &&
(watcherStatistic.isCycleFilenamePrefixTriggerPresent())) {
triggerProcess(watcherStatistic, "->DropZone Filename Prefix Trigger Detected:[" + magicDropZoneFilenamePrefixTrigger + "]");
// Clear Cycle Statistics for this Object.
watcherStatistic.setCycleFilenamePrefixTriggerPresent(false);
watcherStatistic.setCycleFileCreations(0);
watcherStatistic.setCycleFileDeletions(0);
}
} // End of Check for Extract Trigger.
//
// Place other Auto Trigger Events Here, based upon
// Task to be Scheduled or Executed.
//
} // End of Inner Polling Loop per Key
// ********************************************
// reset key and remove from set if directory
// no longer accessible
boolean valid = watchKey.reset();
if (!valid) {
watcherStatistics.remove(watchKey);
// all directories are inaccessible
if (watcherStatistics.isEmpty()) {
break;
}
}
} // Outer while Loop
// Publish a Life Cycle Services Event
event = new LifeCycleServicesEvent(this, LifeCycleServiceType.ZONE,
LifeCycleServiceStateType.DONE, TimeUtils.now());
publisher.publishEvent(event);
logger.warn("Zone Watcher Processing Thread has Ended.");
} // End of run Method
/**
* Common Private Helper method to perform/schedule the Extract process
*
* @param watcherStatistic
* @param whyScheduled - string indicating why we where triggered.
*/
private void triggerProcess(WatcherStatistic watcherStatistic, String whyScheduled) {
logger.warn("Zone Watcher Detected a Cycle of New Extract Files located in the Drop Zone, Scheduling Extract " + whyScheduled);
if (extractProcessingService.isRunning())
{
logger.warn("However, Zone Watcher Detected processing of an Existing Extract already running at this time.");
} else {
//else if ( (lastTimeExtractProcessingTriggered <= 0) ||
// ((TimeUtils.now() - lastTimeExtractProcessingTriggered) >= DEFAULT_EXTRACT_EXECUTION_TIME_WINDOW) ) {
lastTimeExtractProcessingTriggered = TimeUtils.now();
watcherStatistic.setTimeLastTrigger(lastTimeExtractProcessingTriggered);
// Schedule Task for Now.
Calendar cal = Calendar.getInstance();
cal.add(Calendar.SECOND, 60);
schedulerService.scheduleTask(new ExtractProcessingTask(extractProcessingService), cal.getTime());
// } else {
// logger.warn("However, Zone Watcher Detected a Cycle has already been scheduled previously at " +
// TimeUtils.getDate(lastTimeExtractProcessingTriggered)
// + " and will not submit another Extract at this time.");
// }
}
}
} /// End of Inner Class
}