Package restx.classloader

Source Code of restx.classloader.CompilationManager$SourcePath

package restx.classloader;

import com.google.common.base.*;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.google.common.hash.Hashing;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import restx.common.MoreFiles;
import restx.common.watch.FileWatchEvent;
import restx.common.watch.WatcherSettings;

import javax.tools.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.isEmpty;
import static com.google.common.collect.Iterables.transform;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Collections.unmodifiableCollection;

/**
* A compilation manager is responsible for compiling a set of source roots into a
* destination directory.
*
* It is able to scan for changes and compile only modified files, and also watch for changes
* to automatically compile on changes.
*
* It also trigger events whenever a compilation ends.
*/
public class CompilationManager {
    private static final Runnable NO_OP = new Runnable() {
        @Override
        public void run() {
        }
    };
    public static final Predicate<Path> DEFAULT_CLASSPATH_RESOURCE_FILTER = new Predicate<Path>() {
        @Override
        public boolean apply(java.nio.file.Path path) {
            return
                    // Intellij IDEA temporary files
                       !path.toString().endsWith("___jb_old___")
                    && !path.toString().endsWith("___jb_bak___")

                    // svn
                    && path.toAbsolutePath().toString().replace('\\', '/').indexOf("/.svn/") == -1;
        }
    };
    public static final CompilationSettings DEFAULT_SETTINGS = new CompilationSettings() {
        @Override
        public int autoCompileCoalescePeriod() {
            return 50;
        }

        @Override
        public Predicate<Path> classpathResourceFilter() {
            return DEFAULT_CLASSPATH_RESOURCE_FILTER;
        }
    };

    private static final Logger logger = LoggerFactory.getLogger(CompilationManager.class);
    private final EventBus eventBus;
    private final Predicate<Path> classpathResourceFilter;

    private final JavaCompiler javaCompiler;

    private final Iterable<Path> sourceRoots;
    private final Path destination;
    private final CompilationSettings settings;

    private final StandardJavaFileManager fileManager;

    // compile executor is a single thread, we can't perform compilations concurrently
    // all compilation will be done by the compile executor thread
    private final ScheduledExecutorService compileExecutor = Executors.newSingleThreadScheduledExecutor();

    private final ConcurrentLinkedDeque<Path> compileQueue = new ConcurrentLinkedDeque<>();

    private final Map<Path, SourceHash> hashes = new HashMap<>();

    private ExecutorService watcherExecutor;

    private volatile boolean compiling;

    // these parameters should be overridable, at least with system properties
    private final long compilationTimeout = 60; // in seconds
    private final int autoCompileQuietPeriod = 50; // ms
    private final boolean useLastModifiedTocheckChanges = true;
    private Collection<Diagnostic<?>> lastDiagnostics = new CopyOnWriteArrayList<>();

    public CompilationManager(EventBus eventBus, Iterable<Path> sourceRoots, Path destination) {
        this(eventBus, sourceRoots, destination, DEFAULT_SETTINGS);
    }

    public CompilationManager(EventBus eventBus, Iterable<Path> sourceRoots, Path destination, CompilationSettings settings) {
        this.eventBus = checkNotNull(eventBus);
        this.sourceRoots = checkNotNull(sourceRoots);
        this.destination = checkNotNull(destination);
        this.classpathResourceFilter = checkNotNull(settings.classpathResourceFilter());
        this.settings = settings;

        javaCompiler = ToolProvider.getSystemJavaCompiler();
        fileManager = javaCompiler.getStandardFileManager(new DiagnosticCollector<JavaFileObject>(), Locale.ENGLISH, Charsets.UTF_8);
        try {
            if (!destination.toFile().exists()) {
                destination.toFile().mkdirs();
            }

            fileManager.setLocation(StandardLocation.SOURCE_PATH, transform(sourceRoots, MoreFiles.pathToFile));
            fileManager.setLocation(StandardLocation.CLASS_OUTPUT, singleton(destination.toFile()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        loadHashes();

        eventBus.register(new Object() {
            @Subscribe
            public void onWatchEvent(FileWatchEvent event) {
                WatchEvent.Kind<?> kind = event.getKind();
                Path source = event.getDir().resolve(event.getPath());
                if (!source.toFile().isFile()) {
                    return;
                }
                if (isSource(source)) {
                    if (kind == StandardWatchEventKinds.ENTRY_MODIFY
                            || kind == StandardWatchEventKinds.ENTRY_CREATE) {
                        if (!queueCompile(source)) {
                            rebuild();
                        }
                    } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
                        rebuild();
                    } else {
                        rebuild();
                    }
                } else {
                    Optional<SourcePath> sourcePath = SourcePath.resolve(CompilationManager.this.sourceRoots, source);
                    if (sourcePath.isPresent()) {
                        logger.info("classpath resource updated: {}", sourcePath.get().getPath());
                        copyResource(sourcePath.get());
                    }
                }
            }
        });
    }

    public EventBus getEventBus() {
        return eventBus;
    }

    public Path getDestination() {
        return destination;
    }

    public Iterable<Path> getSourceRoots() {
        return sourceRoots;
    }

    private void copyResource(final SourcePath resourcePath) {
        compileExecutor.submit(new Runnable() {
            @Override
            public void run() {
                doCopyResource(resourcePath);
            }
        });
    }

    // IMPORTANT: this should be called in compile executor thread only
    private void doCopyResource(SourcePath resourcePath) {
        File source = resourcePath.toAbsolutePath().toFile();
        if (source.isFile() && classpathResourceFilter.apply(source.toPath())) {
            try {
                File to = destination.resolve(resourcePath.getPath()).toFile();
                to.getParentFile().mkdirs();
                boolean existed = to.exists();
                if (!existed || to.lastModified() < source.lastModified()) {
                    com.google.common.io.Files.copy(source, to);
                    ClasspathResourceEvent.Kind kind = existed ?
                            ClasspathResourceEvent.Kind.UPDATED : ClasspathResourceEvent.Kind.CREATED;
                    eventBus.post(new ClasspathResourceEvent(kind, resourcePath.getPath().toString()));
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private boolean queueCompile(final Path source) {
        boolean b = compileQueue.offerLast(source);
        if (!b) {
            return false;
        }
        compileExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                // nothing added since submission, quiet period is over
                if (compileQueue.getLast() == source) {
                    Collection<Path> sources = new HashSet<>();
                    while (!compileQueue.isEmpty()) {
                        sources.add(compileQueue.removeFirst());
                    }
                    compile(sources);
                }
            }
        }, autoCompileQuietPeriod, TimeUnit.MILLISECONDS);
        return true;
    }


    /**
     * Returns the path of the .class file containing bytecode for the given class (by name).
     *
     * @param className the class for which the class file should be returned
     * @return the Path of the class file, absent if it doesn't exists.
     */
    public Optional<Path> getClassFile(String className) {
        Path classFilePath = destination.resolve(className.replace('.', '/') + ".class");
        if (classFilePath.toFile().exists()) {
            return Optional.of(classFilePath);
        } else {
            return Optional.absent();
        }
    }

    public void startAutoCompile() {
        synchronized (this) {
            if (watcherExecutor == null) {
                watcherExecutor = Executors.newCachedThreadPool();
                Collection<Path> watched = new ArrayList<>();
                for (Path sourceRoot : sourceRoots) {
                    if (sourceRoot.toFile().exists()) {
                        watched.add(sourceRoot);
                        MoreFiles.watch(sourceRoot, eventBus, watcherExecutor, new WatcherSettings() {
                            @Override
                            public int coalescePeriod() {
                                return settings.autoCompileCoalescePeriod();
                            }

                            @Override
                            public boolean recurse() {
                                return true;
                            }
                        });
                    } else {
                        logger.info("source root {} does not exist - IGNORED", sourceRoot);
                    }
                }
                logger.info("watching for changes in {}; current location is {}",
                        watched, new File(".").getAbsoluteFile());
            }
        }
    }

    public void stopAutoCompile() {
        synchronized (this) {
            if (watcherExecutor != null) {
                watcherExecutor.shutdownNow();
                watcherExecutor = null;
            }
        }
    }

    public void awaitAutoCompile() {
        try {
            if (compileQueue.isEmpty()) {
                // nothing in compile queue, we wait for current compilation if any by submitting a noop task
                // and waiting for it
                compileExecutor.submit(NO_OP).get(compilationTimeout, TimeUnit.SECONDS);
            } else {
                // we are in quiet period, let's submit a task after the quiet period and for it
                // if more file changes occur during that period we may miss them, but the purpose of this method
                // is to wait for autoCompile triggered *before* the call.
                compileExecutor.schedule(
                            NO_OP, autoCompileQuietPeriod + 10, TimeUnit.MILLISECONDS)
                        .get(compilationTimeout, TimeUnit.SECONDS);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Performs an incremental compilation.
     */
    public void incrementalCompile() {
        try {
            Exception e = compileExecutor.submit(new Callable<Exception>() {
                @Override
                public Exception call() throws Exception {
                    try {
                        final Collection<Path> sources = new ArrayList<>();
                        for (final Path sourceRoot : sourceRoots) {
                            if (sourceRoot.toFile().exists()) {
                                Files.walkFileTree(sourceRoot, new SimpleFileVisitor<Path>() {
                                    @Override
                                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                                        if (isSource(file)) {
                                            if (hasSourceChanged(sourceRoot, sourceRoot.relativize(file))) {
                                                sources.add(file);
                                            }
                                        } else if (file.toFile().isFile()) {
                                            doCopyResource(SourcePath.valueOf(sourceRoot, sourceRoot.relativize(file)));
                                        }
                                        return FileVisitResult.CONTINUE;
                                    }
                                });
                            }
                        }
                        compile(sources);
                        return null;
                    } catch (Exception e) {
                        return e;
                    }
                }
            }).get(compilationTimeout, TimeUnit.SECONDS);
            if (e != null) {
                throw new RuntimeException(e);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }

    private boolean isSource(Path file) {
        return file.toString().endsWith(".java");
    }

    /**
     * Clean destination and do a full build.
     */
    public void rebuild() {
        try {
            Exception e = compileExecutor.submit(new Callable<Exception>() {
                @Override
                public Exception call() throws Exception {
                    try {
                        compileQueue.clear();
                        MoreFiles.delete(destination);
                        destination.toFile().mkdirs();


                        final Collection<Path> sources = new ArrayList<>();
                        for (final Path sourceRoot : sourceRoots) {
                            if (sourceRoot.toFile().exists()) {
                                Files.walkFileTree(sourceRoot, new SimpleFileVisitor<Path>() {
                                    @Override
                                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                                            throws IOException {
                                        if (isSource(file)) {
                                            sources.add(file);
                                        } else if (file.toFile().isFile()) {
                                            doCopyResource(SourcePath.valueOf(sourceRoot, sourceRoot.relativize(file)));
                                        }
                                        return FileVisitResult.CONTINUE;
                                    }
                                });
                            }
                        }

                        compile(sources);
                        return null;
                    } catch (Exception e) {
                        return e;
                    }
                }
            }).get(compilationTimeout, TimeUnit.SECONDS);
            if (e != null) {
                throw new RuntimeException(e);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }

    public void compileSources(final Path... sources) {
        try {
            Exception e = compileExecutor.submit(new Callable<Exception>() {
                @Override
                public Exception call() throws Exception {
                    compile(asList(sources));
                    return null;
                }
            }).get(compilationTimeout, TimeUnit.SECONDS);
            if (e != null) {
                throw new RuntimeException(e);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }

    public Collection<Diagnostic<?>> getLastDiagnostics() {
        return unmodifiableCollection(lastDiagnostics);
    }

    private final static AtomicLong CLASSLOADER_COUNT = new AtomicLong();

    public HotReloadingClassLoader newHotReloadingClassLoader(String rootPackage, ImmutableSet<Class> coldClasses) {
        try {
            CLASSLOADER_COUNT.incrementAndGet();
            final String name = "HotCompile[" + CLASSLOADER_COUNT + "]";
            final Path destinationDir = getDestination();
            return new HotReloadingClassLoader(
                    new URLClassLoader(
                            new URL[]{destinationDir.toUri().toURL()},
                            Thread.currentThread().getContextClassLoader()),
                    rootPackage, coldClasses
            ) {
                protected InputStream getInputStream(String path) {
                    try {
                        return Files.newInputStream(destinationDir.resolve(path));
                    } catch (IOException e) {
                        return null;
                    }
                }

                @Override
                public String toString() {
                    return name;
                }
            };
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }

    }

    private void compile(Collection<Path> sources) {
        // MUST BE CALLED in compileExecutor only
        Stopwatch stopwatch = Stopwatch.createStarted();
        compiling = true;
        try {
            lastDiagnostics.clear();
            DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
            Iterable<? extends JavaFileObject> javaFileObjects =
                    fileManager.getJavaFileObjectsFromFiles(transform(sources, MoreFiles.pathToFile));

            if (isEmpty(javaFileObjects)) {
                logger.debug("compilation finished: up to date");
                return;
            }
            JavaCompiler.CompilationTask compilationTask = javaCompiler.getTask(
                    null, fileManager, diagnostics, asList("-g"), null, javaFileObjects);

            boolean valid = compilationTask.call();
            if (valid) {
                for (Path source : sources) {
                    Optional<SourcePath> sourcePath = SourcePath.resolve(sourceRoots, source);
                    if (sourcePath.isPresent()) {
                        SourceHash sourceHash = newSourceHashFor(sourcePath.get());
                        hashes.put(source.toAbsolutePath(), sourceHash);

                    }
                }

                saveHashes();

                logger.info("compilation finished: {} sources compiled in {}", sources.size(), stopwatch.stop());
                eventBus.post(new CompilationFinishedEvent(this, DateTime.now(), ImmutableList.copyOf(sources)));
                for (Diagnostic<?> d : diagnostics.getDiagnostics()) {
                    logger.debug("{}", d);
                }
            } else {
                StringBuilder sb = new StringBuilder();
                for (Diagnostic<?> d : diagnostics.getDiagnostics()) {
                    sb.append(d).append("\n");
                }
                lastDiagnostics.addAll(diagnostics.getDiagnostics());
                throw new RuntimeException("Compilation failed:\n" + sb);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            compiling = false;
        }
    }

    private void saveHashes() {
        File hashesFile = hashesFile();
        hashesFile.getParentFile().mkdirs();

        try (Writer w = com.google.common.io.Files.newWriter(hashesFile, Charsets.UTF_8)) {
            for (SourceHash sourceHash : hashes.values()) {
                w.write(sourceHash.serializeAsString());
                w.write("\n");
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void loadHashes() {
        File hashesFile = hashesFile();
        if (hashesFile.exists()) {
            try (BufferedReader r = com.google.common.io.Files.newReader(hashesFile, Charsets.UTF_8)) {
                String line;
                while ((line = r.readLine()) != null) {
                    SourceHash sourceHash = parse(line);
                    hashes.put(sourceHash.getSourcePath().toAbsolutePath(), sourceHash);
                }
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private File hashesFile() {
        return destination.resolve("META-INF/.hashes").toFile();
    }

    /**
     * @return true if this compilation manager is currently performing a compilation task.
     */
    public boolean isCompiling() {
        return compiling;
    }

    private boolean hasSourceChanged(Path dir, Path source) {
        try {
            SourceHash sourceHash = hashes.get(dir.resolve(source).toAbsolutePath());
            if (sourceHash != null) {
                return sourceHash.hasChanged() != sourceHash;
            } else {
                return true;
            }
        } catch (IOException e) {
            return true;
        }
    }

    private static class SourcePath {
        public static Optional<SourcePath> resolve(Iterable<Path> sourceRoots, Path source) {
            Path dir = null;
            for (Path sourceRoot : sourceRoots) {
                if ((source.isAbsolute() && source.startsWith(sourceRoot.toAbsolutePath()))
                        || (!source.isAbsolute() && source.startsWith(sourceRoot))) {
                    dir = sourceRoot;
                    break;
                }
            }
            if (dir == null) {
                logger.warn("can't find sourceRoot for {}", source);
                return Optional.absent();
            } else {
                return Optional.of(new SourcePath(dir, source.isAbsolute() ?
                                        dir.toAbsolutePath().relativize(source) :
                                        dir.relativize(source)));
            }
        }

        public static SourcePath valueOf(Path sourceRoot, Path path) {
            return new SourcePath(sourceRoot, path);
        }

        private final Path sourceDir;
        private final Path path;

        private SourcePath(Path sourceDir, Path path) {
            this.sourceDir = sourceDir;
            this.path = path;
        }

        public Path getSourceDir() {
            return sourceDir;
        }

        public Path getPath() {
            return path;
        }

        public Path toAbsolutePath() {
            return sourceDir.resolve(path).toAbsolutePath();
        }

        @Override
        public String toString() {
            return "SourcePath{" +
                    "sourceDir=" + sourceDir +
                    ", path=" + path +
                    '}';
        }
    }

    private class SourceHash {
        private final SourcePath sourcePath;
        private final String hash;
        private final long lastModified;

        private SourceHash(SourcePath sourcePath, String hash, long lastModified) {
            this.sourcePath = sourcePath;
            this.hash = hash;
            this.lastModified = lastModified;
        }

        @Override
        public String toString() {
            return "SourceHash{" +
                    "sourcePath=" + sourcePath +
                    ", hash='" + hash + '\'' +
                    ", lastModified=" + lastModified +
                    '}';
        }

        public SourcePath getSourcePath() {
            return sourcePath;
        }

        public String getHash() {
            return hash;
        }

        public long getLastModified() {
            return lastModified;
        }

        public SourceHash hasChanged() throws IOException {
            File sourceFile = sourcePath.toAbsolutePath().toFile();
            if (useLastModifiedTocheckChanges) {
                if (lastModified < sourceFile.lastModified()) {
                    return new SourceHash(sourcePath,
                            computeHash(), sourceFile.lastModified());
                }
            } else {
                String currentHash = computeHash();
                if (!currentHash.equals(hash)) {
                    return new SourceHash(sourcePath,
                            currentHash, sourceFile.lastModified());
                }
            }
            return this;
        }

        private String computeHash() throws IOException {
            return hash(sourcePath.toAbsolutePath().toFile());
        }

        public String serializeAsString() throws IOException {
            return Joiner.on("**").join(sourcePath.getSourceDir(), sourcePath.getPath(), hash, lastModified);
        }
    }

    private SourceHash newSourceHashFor(SourcePath sourcePath) throws IOException {
        File sourceFile = sourcePath.toAbsolutePath().toFile();
        return new SourceHash(sourcePath, hash(sourceFile), sourceFile.lastModified());
    }

    private String hash(File file) throws IOException {
        return com.google.common.io.Files.hash(file, Hashing.md5()).toString();
    }

    private SourceHash parse(String str) {
        Iterator<String> parts = Splitter.on("**").split(str).iterator();
        FileSystem fileSystem = FileSystems.getDefault();
        return new SourceHash(
                SourcePath.valueOf(fileSystem.getPath(parts.next()),
                               fileSystem.getPath(parts.next())),
                parts.next(),
                Long.parseLong(parts.next())
        );
    }
}
TOP

Related Classes of restx.classloader.CompilationManager$SourcePath

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.