DirWatchService.groovy
/*
Copyright 2012-now Jex Jexler (Alain Stalder)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ch.grengine.jexler.service
import ch.grengine.jexler.Jexler
import groovy.transform.CompileStatic
import org.quartz.CronScheduleBuilder
import org.quartz.Job
import org.quartz.JobBuilder
import org.quartz.JobDataMap
import org.quartz.JobDetail
import org.quartz.JobExecutionContext
import org.quartz.JobExecutionException
import org.quartz.Scheduler
import org.quartz.Trigger
import org.quartz.TriggerBuilder
import org.quartz.TriggerKey
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.nio.file.WatchService
import static ch.grengine.jexler.service.ServiceState.IDLE
import static ch.grengine.jexler.service.ServiceState.OFF
/**
* Directory watch service, creates an event when a file
* in a given directory is created, modified oder deleted.
*
* @author Jex Jexler (Alain Stalder)
*/
@CompileStatic
class DirWatchService extends ServiceBase {
private static final Logger LOG = LoggerFactory.getLogger(DirWatchService.class)
private final Jexler jexler
private File dir
private List<WatchEvent.Kind<Path>> kinds
private List<WatchEvent.Modifier> modifiers
private String cron
private Scheduler scheduler
private TriggerKey triggerKey
private WatchService watchService
private WatchKey watchKey
/**
* Constructor.
* @param jexler the jexler to send events to
* @param id the id of the service
*/
DirWatchService(final Jexler jexler, final String id) {
super(id)
this.jexler = jexler
dir = jexler.dir
kinds = [ StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE ]
modifiers = []
cron = '*/5 * * * * ?'
}
/**
* Set directory to watch.
* Default if not set is the directory that contains the jexler.
* @param dir directory to watch
* @return this (for chaining calls)
*/
DirWatchService setDir(final File dir) {
this.dir = dir
return this
}
/**
* Get directory to watch.
*/
File getDir() {
return dir
}
/**
* Set kinds of events to watch for.
* Default is standard events for create, modify and delete.
* @param kinds
* @return
*/
DirWatchService setKinds(final List<WatchEvent.Kind<Path>> kinds) {
this.kinds = kinds
return this
}
/**
* Get kinds of events to watch for.
*/
List<WatchEvent.Kind<Path>> getKinds() {
return kinds
}
/**
* Set modifiers when watching for events.
* On Mac OS X, by default the file system seems to be polled
* every 10 seconds; to reduce this to 2 seconds, pass a modifier
* <code>com.sun.nio.file.SensitivityWatchEventModifier.HIGH</code>.
* @param modifiers
* @return
*/
DirWatchService setModifiers(final List<WatchEvent.Modifier> modifiers) {
this.modifiers = modifiers
return this
}
/**
* Get modifiers when watching for events.
*/
List<WatchEvent.Modifier> getModifiers() {
return modifiers
}
/**
* Set cron pattern for when to check.
* Default is every 5 seconds.
* @return this (for chaining calls)
*/
DirWatchService setCron(final String cron) {
this.cron = ServiceUtil.toQuartzCron(cron)
return this
}
/**
* Get cron pattern.
*/
String getCron() {
return cron
}
/**
* Set quartz scheduler.
* Default is a scheduler shared by all jexlers in the same jexler container.
* @return this (for chaining calls)
*/
DirWatchService setScheduler(final Scheduler scheduler) {
this.scheduler = scheduler
return this
}
/**
* Get quartz scheduler.
*/
Scheduler getScheduler() {
return scheduler
}
/**
* Get jexler.
*/
Jexler getJexler() {
return jexler
}
@Override
void start() {
if (state.on) {
return
}
final Path path = dir.toPath()
try {
watchService = path.fileSystem.newWatchService()
watchKey = path.register(watchService,
kinds as WatchEvent.Kind[],
modifiers as WatchEvent.Modifier[])
} catch (final IOException eCreate) {
jexler.trackIssue(this,
"Could not create watch service or key for directory '$dir.absolutePath'.", eCreate)
return
}
final String uuid = UUID.randomUUID()
final JobDetail job = JobBuilder.newJob(DirWatchJob.class)
.withIdentity("job-$id-$uuid", jexler.id)
.usingJobData(['service':this] as JobDataMap)
.build()
final Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger-$id-$uuid", jexler.id)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.startNow()
.build()
triggerKey = trigger.key
if (scheduler == null) {
scheduler = jexler.container.scheduler
}
scheduler.scheduleJob(job, trigger)
state = IDLE
}
@Override
void stop() {
if (state.off) {
return
}
scheduler.unscheduleJob(triggerKey)
watchKey.cancel()
try {
watchService.close()
} catch (final IOException e) {
LOG.trace('failed to close watch service', e)
}
state = OFF
}
@Override
void zap() {
if (state.off) {
return
}
state = OFF
new Thread() {
void run() {
if (scheduler != null) {
try {
scheduler.unscheduleJob(triggerKey)
} catch (final Throwable tUnschedule) {
LOG.trace('failed to unschedule cron job', tUnschedule)
}
}
try {
watchKey.cancel()
watchService.close()
} catch (final Throwable tStop) {
LOG.trace('failed stop watching directory', tStop)
}
}
}.start()
}
/**
* Internal class, only public because otherwise not called by quartz scheduler.
*/
static class DirWatchJob implements Job {
void execute(final JobExecutionContext ctx) throws JobExecutionException {
final DirWatchService service = (DirWatchService)ctx.jobDetail.jobDataMap.service
final String savedName = Thread.currentThread().name
Thread.currentThread().name = "$service.jexler.id|$service.id"
for (final WatchEvent watchEvent : service.watchKey.pollEvents()) {
final Path contextPath = ((Path) watchEvent.context())
final File file = new File(service.dir, contextPath.toFile().name)
final WatchEvent.Kind kind = watchEvent.kind()
LOG.trace("event $kind '$file.absolutePath'")
service.jexler.handle(new DirWatchEvent(service, file, kind))
}
Thread.currentThread().name = savedName
}
}
}