io.mangoo.build.Watcher.java Source code

Java tutorial

Introduction

Here is the source code for io.mangoo.build.Watcher.java

Source

/**
 * 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;
    }
}