/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* http://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 org.apache.wicket.core.util.watch;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import org.apache.wicket.Application;
import org.apache.wicket.ThreadContext;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.util.io.IOUtils;
import org.apache.wicket.util.string.Strings;
import org.apache.wicket.util.thread.ICode;
import org.apache.wicket.util.thread.Task;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.watch.ModificationWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An extension of ModificationWatcher that removes the NotFound entries from
* the MarkupCache for newly created files.
*
* By default MarkupCache registers Markup.NO_MARKUP value for each requested but
* not found markup file. Later when the user creates the markup file the MarkupCache
* should be notified.
*
* @since 7.0.0
*/
public class Nio2ModificationWatcher extends ModificationWatcher
{
private static final Logger LOG = LoggerFactory.getLogger(Nio2ModificationWatcher.class);
private final WatchService watchService;
private final Application application;
/** the <code>Task</code> to run */
private Task task;
/**
* Constructor.
*
* @param application
* The application that manages the caches
* @param pollFrequency
* How often to check on <code>IModifiable</code>s
*/
public Nio2ModificationWatcher(final Application application, Duration pollFrequency)
{
try
{
this.application = application;
this.watchService = FileSystems.getDefault().newWatchService();
registerWatchables(watchService);
start(pollFrequency);
} catch (IOException iox)
{
throw new WicketRuntimeException("Cannot get the watch service", iox);
}
}
@Override
public void start(final Duration pollFrequency)
{
// Construct task with the given polling frequency
task = new Task("Wicket-ModificationWatcher-NIO2");
task.run(pollFrequency, new ICode() {
@Override
public void run(final Logger log)
{
checkCreated(log);
checkModified();
}
});
}
/**
* Checks for newly created files and folders.
* New folders are registered to be watched.
* New files are removed from the MarkupCache because there could be
* {@link org.apache.wicket.markup.Markup#NO_MARKUP} (Not Found) entries for them already.
* @param log
* a logger that can be used to log the events
*/
protected void checkCreated(Logger log)
{
WatchKey watchKey = watchService.poll();
if (watchKey != null)
{
List<WatchEvent<?>> events = watchKey.pollEvents();
for (WatchEvent<?> event : events)
{
WatchEvent.Kind<?> eventKind = event.kind();
Path eventPath = (Path) event.context();
if (eventKind == ENTRY_CREATE)
{
entryCreated(eventPath, log);
}
else if (eventKind == ENTRY_DELETE)
{
entryDeleted(eventPath, log);
}
else if (eventKind == ENTRY_MODIFY)
{
entryModified(eventPath, log);
}
}
watchKey.reset();
}
}
/**
* A callback method called when a new Path entry is modified
*
* @param path
* the modified path
* @param log
* a logger that can be used to log the events
*/
protected void entryModified(Path path, Logger log)
{
}
/**
* A callback method called when a new Path entry is deleted
*
* @param path
* the deleted path
* @param log
* a logger that can be used to log the events
*/
protected void entryDeleted(Path path, Logger log)
{
}
/**
* A callback method called when a new Path entry is created
*
* @param path
* the new path entry
* @param log
* a logger that can be used to log the events
*/
protected void entryCreated(Path path, Logger log)
{
if (Files.isDirectory(path))
{
try
{
// a directory is created. register it for notifications
register(path, watchService);
} catch (IOException iox)
{
log.warn("Cannot register folder '" + path + "' to be watched.", iox);
}
}
else
{
// A new file is created. We need to clear the NOT_FOUND entry that may have been added earlier.
// MarkupCache keys are fully qualified URIs
String absolutePath = path.toAbsolutePath().toFile().toURI().toString();
try
{
ThreadContext.setApplication(application);
application.getMarkupSettings()
.getMarkupFactory().getMarkupCache().removeMarkup(absolutePath);
} finally {
ThreadContext.setApplication(null);
}
}
}
@Override
public void destroy()
{
try
{
super.destroy();
if (task != null)
{
task.interrupt();
}
} finally
{
IOUtils.closeQuietly(watchService);
}
}
/**
* Registers all classpath folder entries and their subfolders in the {@code #watchService}.
*
* @param watchService
* the watch service that will send the notifications
* @throws IOException
*/
private void registerWatchables(final WatchService watchService) throws IOException
{
String classpath = System.getProperty("java.class.path");
String[] classPathEntries = Strings.split(classpath, File.pathSeparatorChar);
for (String classPathEntry : classPathEntries)
{
if (classPathEntry.endsWith(".jar") == false)
{
Path folder = Paths.get(classPathEntry);
if (Files.isDirectory(folder))
{
register(folder, watchService);
Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
register(dir, watchService);
return FileVisitResult.CONTINUE;
}
});
}
}
}
}
private void register(final Path folder, final WatchService watchService) throws IOException
{
WatchEvent.Kind[] watchedKinds = getWatchedKinds(folder);
LOG.debug("Registering folder '{}' to the watching service with kinds: {}", folder, watchedKinds);
folder.register(watchService, watchedKinds);
}
/**
* @param folder
* the folder that will be watched
* @return an array of watch event kinds to use for the watching of the given folder
*/
protected WatchEvent.Kind[] getWatchedKinds(Path folder)
{
return new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
}
}