org.terasology.assets.module.ModuleWatcher.java Source code

Java tutorial

Introduction

Here is the source code for org.terasology.assets.module.ModuleWatcher.java

Source

/*
 * Copyright 2015 MovingBlocks
 *
 * 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 org.terasology.assets.module;

import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Queues;
import com.google.common.collect.SetMultimap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.AssetType;
import org.terasology.assets.ResourceUrn;
import org.terasology.module.Module;
import org.terasology.module.ModuleEnvironment;
import org.terasology.naming.Name;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
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 java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingDeque;

/**
 * Centralised detector of changes to asset files (creation, modification, deletion), to inform ModuleAssetDataProducers of the changes
 * and trigger the reload of those assets.
 * <p>
 * One small note: Sometimes it is possible to get file change notifications before the change has actually happened. I observed this under windows
 * when using the New->Folder/New->File optional in explorer - the path will report as not existing unless you wait a short period of time. To address
 * this those events are added into a unreadyEvents queue and processed next check.
 * </p>
 *
 * @author Immortius
 */
class ModuleWatcher {

    private static final Logger logger = LoggerFactory.getLogger(ModuleWatcher.class);

    private final WatchService service;

    private final Map<WatchKey, PathWatcher> pathWatchers = new MapMaker().concurrencyLevel(1).makeMap();
    private final Map<Path, WatchKey> watchKeys = new MapMaker().concurrencyLevel(1).makeMap();
    private final ListMultimap<String, SubscriberInfo> subscribers = Multimaps
            .synchronizedListMultimap(ArrayListMultimap.create());
    private final BlockingDeque<DelayedEvent> unreadyEvents = Queues.newLinkedBlockingDeque();

    private boolean closed;

    /**
     * Creates a ModuleWatcher for the given module environment
     *
     * @param environment The environment to watch.
     * @throws IOException If there is an issue establishing the watch service
     */
    public ModuleWatcher(ModuleEnvironment environment) throws IOException {
        this.service = environment.getFileSystem().newWatchService();

        for (Path rootPath : environment.getFileSystem().getRootDirectories()) {
            try {
                Module module = environment.get(new Name(rootPath.getName(0).toString()));
                boolean canWatch = module.getLocations().stream()
                        .anyMatch(location -> FileSystems.getDefault().equals(location.getFileSystem()));
                if (canWatch) {
                    PathWatcher watcher = new RootPathWatcher(rootPath, module.getId(), service);
                    watcher.onRegistered();
                }
            } catch (IOException e) {
                logger.warn("Failed to establish change watch service for path '{}'", rootPath, e);
            }
        }
    }

    /**
     * Registers an subscriber to a specific asset folder, for a given asset type.
     *
     * @param folderName The name of the folder to subscribe to - under which assets of interest lie (e.g. "textures")
     * @param subscriber The subscriber to notify of changes
     * @param assetType  The asset type whom changes urns belong to
     */
    public synchronized void register(String folderName, AssetFileChangeSubscriber subscriber,
            AssetType<?, ?> assetType) {
        Preconditions.checkState(!closed, "Cannot register folder into closed ModuleWatcher");
        subscribers.put(folderName, new SubscriberInfo(assetType, subscriber));
    }

    public synchronized boolean isClosed() {
        return closed;
    }

    /**
     * Shuts down the ModuleWatcher.
     *
     * @throws IOException If there is an error shutting down the service
     */
    public synchronized void shutdown() throws IOException {
        if (!closed) {
            pathWatchers.clear();
            watchKeys.clear();
            service.close();
            closed = true;
        }
    }

    /**
     * Checks the file system for any changes that affects assets.
     *
     * @return A set of ResourceUrns of changed assets.
     */
    public synchronized SetMultimap<AssetType<?, ?>, ResourceUrn> checkForChanges() {
        if (closed) {
            return LinkedHashMultimap.create();
        }

        SetMultimap<AssetType<?, ?>, ResourceUrn> changed = LinkedHashMultimap.create();

        List<DelayedEvent> events = Lists.newArrayList();
        unreadyEvents.drainTo(events);
        for (DelayedEvent event : events) {
            changed.putAll(event.replay());
        }

        WatchKey key = service.poll();
        while (key != null) {
            PathWatcher pathWatcher = pathWatchers.get(key);
            changed.putAll(pathWatcher.update(key.pollEvents(), Optional.of(unreadyEvents)));
            key.reset();
            key = service.poll();
        }
        return changed;
    }

    /**
     * Notifies subscribers of an asset file change
     *
     * @param folderName      The asset folder in which the changed asset resides
     * @param target          The path of the file
     * @param module          The module the file contributes to
     * @param providingModule The module that provides the file
     * @param method          The subscription method to call to notify
     * @param outChanged      A map of asset types and their changed urns to add any modified resource urns to.
     */
    private void notifySubscribers(String folderName, Path target, Name module, Name providingModule,
            SubscriptionMethod method, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
        for (SubscriberInfo subscriber : subscribers.get(folderName)) {

            Optional<ResourceUrn> urn = method.notify(subscriber.subscriber, target, module, providingModule);
            if (urn.isPresent()) {
                outChanged.put(subscriber.type, urn.get());
            }
        }
    }

    /**
     * Information on a subscriber.
     */
    private static class SubscriberInfo {
        public final AssetType<?, ?> type;
        public final AssetFileChangeSubscriber subscriber;

        public SubscriberInfo(AssetType<?, ?> type, AssetFileChangeSubscriber subscriber) {
            this.type = type;
            this.subscriber = subscriber;
        }
    }

    /**
     * The form of a method to call to notify a subscriber of changes
     */
    private interface SubscriptionMethod {
        Optional<ResourceUrn> notify(AssetFileChangeSubscriber subscriber, Path path, Name module,
                Name providingModule);
    }

    /**
     * A PathWatcher watches a path for changes, and reacts to those changes.
     */
    private abstract class PathWatcher {
        private Path watchedPath;
        private WatchService watchService;

        public PathWatcher(Path path, WatchService watchService) throws IOException {
            this.watchedPath = path;
            this.watchService = watchService;
            WatchKey key = path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY,
                    StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
            if (key.isValid()) {
                pathWatchers.put(key, this);
                watchKeys.put(path, key);
            }
        }

        public Path getWatchedPath() {
            return watchedPath;
        }

        public WatchService getWatchService() {
            return watchService;
        }

        @SuppressWarnings("unchecked")
        public final SetMultimap<AssetType<?, ?>, ResourceUrn> update(List<WatchEvent<?>> watchEvents,
                Optional<Collection<DelayedEvent>> outDelayedEvents) {
            final SetMultimap<AssetType<?, ?>, ResourceUrn> changedAssets = LinkedHashMultimap.create();
            for (WatchEvent<?> event : watchEvents) {
                WatchEvent.Kind kind = event.kind();
                if (kind == StandardWatchEventKinds.OVERFLOW) {
                    logger.warn("File event overflow - lost change events");
                    continue;
                }

                WatchEvent<Path> pathEvent = (WatchEvent<Path>) event;
                Path target = watchedPath.resolve(pathEvent.context());
                if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                    if (Files.isDirectory(target)) {
                        logger.debug("New directory registered: {}", target);
                        onDirectoryCreated(target, changedAssets);
                    } else if (Files.isRegularFile(target)) {
                        onFileCreated(target, changedAssets);
                    } else if (outDelayedEvents.isPresent()) {
                        outDelayedEvents.get().add(new DelayedEvent(event, this));
                    }
                } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    if (Files.isRegularFile(target)) {
                        onFileModified(target, changedAssets);
                    }
                } else if (event.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
                    WatchKey key = watchKeys.remove(target);
                    if (key != null) {
                        pathWatchers.remove(key);
                    } else {
                        onFileDeleted(target, changedAssets);
                    }
                }
            }
            return changedAssets;
        }

        private void onDirectoryCreated(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            try {
                Optional<? extends PathWatcher> pathWatcher = processPath(target);
                if (pathWatcher.isPresent()) {
                    pathWatcher.get().onCreated(outChanged);
                }
            } catch (IOException e) {
                logger.error("Error registering path for change watching '{}'", getWatchedPath(), e);
            }
        }

        /**
         * Called when the path watcher is registered for an existing path
         */
        public final void onRegistered() {
            try (DirectoryStream<Path> contents = Files.newDirectoryStream(getWatchedPath())) {
                for (Path path : contents) {
                    if (Files.isDirectory(path)) {
                        Optional<? extends PathWatcher> pathWatcher = processPath(path);
                        if (pathWatcher.isPresent()) {
                            pathWatcher.get().onRegistered();
                        }
                    }
                }
            } catch (IOException e) {
                logger.error("Error registering path for change watching '{}'", getWatchedPath(), e);
            }
        }

        /**
         * Called when the path watcher is for a newly created path
         *
         * @param outChanged The ResourceUrns of any assets affected by the creation of this path
         */
        public final void onCreated(SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            try (DirectoryStream<Path> contents = Files.newDirectoryStream(getWatchedPath())) {
                for (Path path : contents) {
                    if (Files.isDirectory(path)) {
                        onDirectoryCreated(path, outChanged);
                    } else {
                        onFileCreated(path, outChanged);
                    }
                }
            } catch (IOException e) {
                logger.error("Error registering path for change watching '{}'", getWatchedPath(), e);
            }
        }

        /**
         * Processes a path within this path watcher
         *
         * @param target The path to process
         * @return A new path watcher for the path
         * @throws IOException If there was any issue processing the path
         */
        protected abstract Optional<? extends PathWatcher> processPath(Path target) throws IOException;

        /**
         * Called when a file is created
         *
         * @param target     The created file
         * @param outChanged The ResourceUrns of any assets affected
         */
        protected void onFileCreated(Path target, final SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
        }

        /**
         * Called when a file is modified
         *
         * @param target     The modified file
         * @param outChanged The ResourceUrns of any assets affected
         */
        protected void onFileModified(Path target, final SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
        }

        /**
         * Called when a file is deleted
         *
         * @param target     The deleted file
         * @param outChanged The ResourceUrns of any assets affected
         */
        protected void onFileDeleted(Path target, final SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
        }
    }

    private class RootPathWatcher extends PathWatcher {

        private Name module;

        public RootPathWatcher(Path path, Name module, WatchService watchService) throws IOException {
            super(path, watchService);
            this.module = module;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            if (target.getNameCount() == 2) {
                switch (target.getName(1).toString()) {
                case ModuleAssetDataProducer.ASSET_FOLDER: {
                    return Optional.of(new AssetRootPathWatcher(target, module, getWatchService()));
                }
                case ModuleAssetDataProducer.DELTA_FOLDER: {
                    return Optional.of(new DeltaRootPathWatcher(target, module, getWatchService()));
                }
                case ModuleAssetDataProducer.OVERRIDE_FOLDER: {
                    return Optional.of(new OverrideRootPathWatcher(target, module, getWatchService()));
                }
                }
            }
            return Optional.empty();
        }
    }

    private class AssetRootPathWatcher extends PathWatcher {

        private Name module;

        public AssetRootPathWatcher(Path path, Name module, WatchService watchService) throws IOException {
            super(path, watchService);
            this.module = module;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            if (target.getNameCount() == 3) {
                return Optional.of(new AssetPathWatcher(target, target.getName(2).toString(), module, module,
                        getWatchService()));
            }
            return Optional.empty();
        }
    }

    private class OverrideRootPathWatcher extends PathWatcher {

        private Name module;

        public OverrideRootPathWatcher(Path path, Name module, WatchService watchService) throws IOException {
            super(path, watchService);
            this.module = module;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            if (target.getNameCount() == 3) {
                return Optional.of(new OverrideRootPathWatcher(target, module, getWatchService()));
            } else if (target.getNameCount() == 4) {
                return Optional.of(new AssetPathWatcher(target, target.getName(3).toString(),
                        new Name(target.getName(2).toString()), module, getWatchService()));
            }
            return Optional.empty();
        }
    }

    private class DeltaRootPathWatcher extends PathWatcher {

        private Name module;

        public DeltaRootPathWatcher(Path path, Name module, WatchService watchService) throws IOException {
            super(path, watchService);
            this.module = module;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            if (target.getNameCount() == 3) {
                return Optional.of(new DeltaRootPathWatcher(target, module, getWatchService()));
            } else if (target.getNameCount() == 4) {
                return Optional.of(new DeltaPathWatcher(target, new Name(target.getName(2).toString()), module,
                        getWatchService()));
            }
            return Optional.empty();
        }
    }

    private class DeltaPathWatcher extends PathWatcher {

        private final Name providingModule;
        private final Name module;

        public DeltaPathWatcher(Path path, Name module, Name providingModule, WatchService watchService)
                throws IOException {
            super(path, watchService);
            this.module = module;
            this.providingModule = providingModule;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            return Optional.of(new DeltaPathWatcher(target, module, providingModule, getWatchService()));
        }

        @Override
        protected void onFileCreated(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Delta added: {}", target);
            String folderName = target.getName(3).toString();
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::deltaFileAdded, outChanged);
        }

        @Override
        protected void onFileModified(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Delta modified: {}", target);
            String folderName = target.getName(3).toString();
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::deltaFileModified, outChanged);
        }

        @Override
        protected void onFileDeleted(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Delta deleted: {}", target);
            String folderName = target.getName(3).toString();
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::deltaFileDeleted, outChanged);
        }
    }

    private class AssetPathWatcher extends PathWatcher {

        private String folderName;
        private Name module;
        private Name providingModule;

        public AssetPathWatcher(Path path, String folderName, Name module, Name providingModule,
                WatchService watchService) throws IOException {
            super(path, watchService);
            this.folderName = folderName;
            this.module = module;
            this.providingModule = providingModule;
        }

        @Override
        protected Optional<? extends PathWatcher> processPath(Path target) throws IOException {
            return Optional
                    .of(new AssetPathWatcher(target, folderName, module, providingModule, getWatchService()));
        }

        @Override
        protected void onFileCreated(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Asset added: {}", target);
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::assetFileAdded, outChanged);
        }

        @Override
        protected void onFileModified(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Asset modified: {}", target);
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::assetFileModified, outChanged);
        }

        @Override
        protected void onFileDeleted(Path target, SetMultimap<AssetType<?, ?>, ResourceUrn> outChanged) {
            logger.debug("Asset deleted: {}", target);
            notifySubscribers(folderName, target, module, providingModule,
                    AssetFileChangeSubscriber::assetFileDeleted, outChanged);
        }
    }

    private static class DelayedEvent {
        private WatchEvent<?> event;
        private PathWatcher watcher;

        public DelayedEvent(WatchEvent<?> event, PathWatcher watcher) {
            this.event = event;
            this.watcher = watcher;
        }

        public SetMultimap<AssetType<?, ?>, ResourceUrn> replay() {
            return watcher.update(Arrays.asList(event), Optional.empty());
        }
    }

}