/**
* Copyright (C) 2012-2015 the original author or authors.
*
* 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
*
* 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 io.mangoo.build;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
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.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.sun.nio.file.SensitivityWatchEventModifier; //NOSONAR
import io.mangoo.enums.Suffix;
import io.mangoo.utils.MinificationUtils;
/**
* This is a refactored version of
* WatchAndRestartMachine.java from the Ninja Web Framework
*
* Original source code can be found here:
* https://github.com/ninjaframework/ninja/blob/develop/ninja-maven-plugin/src/main/java/ninja/build/WatchAndRestartMachine.java
*
* @author svenkubiak
*
*/
@SuppressWarnings({"restriction", "unchecked"})
public class Watcher implements Runnable {
private static final Logger LOG = LogManager.getLogger(Watcher.class);
private final Trigger trigger;
private final Set<String> includes;
private final Set<String> excludes;
private final WatchService watchService;
private final Map<WatchKey, Path> watchKeys;
private final AtomicInteger takeCount;
private boolean shutdown;
@SuppressWarnings("all")
public Watcher(Set<Path> watchDirectory, Set<String> includes, Set<String> excludes, Trigger trigger) throws IOException {
this.watchService = FileSystems.getDefault().newWatchService();
this.watchKeys = new HashMap<>();
this.includes = includes; //NOSONAR
this.excludes = excludes; //NOSONAR
this.trigger = trigger;
this.takeCount = new AtomicInteger(0);
for (Path path: watchDirectory) {
registerAll(path);
}
}
public void doShutdown() {
this.shutdown = true;
}
@SuppressWarnings("all")
private void registerAll(final Path path) throws IOException {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) throws IOException {
register(path);
return FileVisitResult.CONTINUE;
}
});
}
/**
*
* USUALLY THIS IS THE DEFAULT WAY TO REGISTER THE EVENTS:
*
* WatchKey watchKey = path.register(
* watchService,
* ENTRY_CREATE,
* ENTRY_DELETE,
* ENTRY_MODIFY);
*
* BUT THIS IS DAMN SLOW (at least on a Mac)
* THEREFORE WE USE EVENTS FROM COM.SUN PACKAGES THAT ARE WAY FASTER
* THIS MIGHT BREAK COMPATIBILITY WITH OTHER JDKs
* MORE: http://stackoverflow.com/questions/9588737/is-java-7-watchservice-slow-for-anyone-else
*
* @param path
* @throws IOException
*/
private void register(Path path) throws IOException {
WatchKey watchKey = path.register(
watchService,
new WatchEvent.Kind[]{
StandardWatchEventKinds.ENTRY_CREATE, //NOSONAR
StandardWatchEventKinds.ENTRY_MODIFY, //NOSONAR
StandardWatchEventKinds.ENTRY_DELETE //NOSONAR
},
SensitivityWatchEventModifier.HIGH);
watchKeys.put(watchKey, path);
}
@Override
@SuppressWarnings("all")
public void run() {
for (;;) {
WatchKey watchKey;
try {
watchKey = watchService.take();
takeCount.incrementAndGet();
} catch (InterruptedException e) {
if (!shutdown) {
LOG.error("Unexpectedly interrupted while waiting for take()", e);
}
return;
}
Path path = watchKeys.get(watchKey);
if (path == null) {
LOG.error("WatchKey not recognized!!");
continue;
}
handleEvents(watchKey, path);
if (!watchKey.reset()) {
watchKeys.remove(watchKey);
if (watchKeys.isEmpty()) {
break;
}
}
}
}
@SuppressWarnings("all")
private void handleEvents(WatchKey watchKey, Path path) {
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
WatchEvent.Kind<?> watchEventKind = watchEvent.kind();
if (OVERFLOW.equals(watchEventKind)) {
continue;
}
WatchEvent<Path> ev = (WatchEvent<Path>) watchEvent;
Path name = ev.context();
Path child = path.resolve(name);
if (ENTRY_MODIFY.equals(watchEventKind) && !child.toFile().isDirectory()) {
handleNewOrModifiedFile(child);
}
if (ENTRY_CREATE.equals(watchEventKind)) {
if (!child.toFile().isDirectory()) {
handleNewOrModifiedFile(child);
}
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(child);
}
} catch (IOException e) {
LOG.error("Something fishy happened. Unable to register new dir for watching", e);
}
}
}
}
public void handleNewOrModifiedFile(Path path) {
String absolutePath = path.toFile().getAbsolutePath();
if (isPreprocess(absolutePath)){
MinificationUtils.preprocess(absolutePath);
String [] tempPath = absolutePath.split("files");
MinificationUtils.minify(tempPath[0] + "files/assets/stylesheet/" + StringUtils.substringAfterLast(absolutePath, "/")
.replace(Suffix.SASS.toString(), Suffix.CSS.toString())
.replace(Suffix.LESS.toString(), Suffix.CSS.toString()));
}
if (isAsset(absolutePath)) {
MinificationUtils.minify(absolutePath);
}
RuleMatch match = matchRule(includes, excludes, absolutePath);
if (match.proceed) {
this.trigger.trigger();
}
}
private boolean isAsset(String absolutePath) {
if (StringUtils.isBlank(absolutePath)) {
return false;
}
return !absolutePath.contains("min") && ( absolutePath.endsWith("css") || absolutePath.endsWith("js") );
}
private boolean isPreprocess(String absolutePath) {
if (StringUtils.isBlank(absolutePath)) {
return false;
}
return absolutePath.endsWith("sass") || absolutePath.endsWith("less");
}
public enum RuleType {
NONE,
INCLUDE,
EXCLUDE
}
public static class RuleMatch {
private final boolean proceed;
public RuleMatch(boolean proceed) {
this.proceed = proceed;
}
}
public static RuleMatch matchRule(Set<String> includes, Set<String> excludes, String string) {
if (includes != null) {
for (String regex: includes) {
if (string.matches(regex)) {
return new RuleMatch(true);
}
}
}
if (excludes != null) {
for (String exclude : excludes) {
if (string.matches(exclude)) {
return new RuleMatch(false);
}
}
}
return new RuleMatch(true);
}
public static boolean checkIfWouldBeExcluded(Set<String> patterns, String string) {
return !matchRule(null, patterns, string).proceed;
}
}