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

Java tutorial

Introduction

Here is the source code for org.terasology.assets.module.ModuleAssetDataProducer.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.Charsets;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.io.CharStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.assets.AssetData;
import org.terasology.assets.AssetDataProducer;
import org.terasology.assets.ResourceUrn;
import org.terasology.assets.exceptions.InvalidAssetFilenameException;
import org.terasology.assets.format.AssetAlterationFileFormat;
import org.terasology.assets.format.AssetFileFormat;
import org.terasology.assets.format.FileFormat;
import org.terasology.module.Module;
import org.terasology.module.ModuleEnvironment;
import org.terasology.module.filesystem.ModuleFileSystemProvider;
import org.terasology.naming.Name;
import org.terasology.util.io.FileExtensionPathMatcher;
import org.terasology.util.io.FileScanning;

import javax.annotation.concurrent.ThreadSafe;
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

/**
 * ModuleAssetDataProducer produces asset data from files within modules. In addition to files defining assets, it supports
 * files that override or alter assets defined in other modules, files redirecting a urn to another urn, and the ability
 * to make modifications to asset files in the file system that can be detected and used to reload assets.
 * <p>
 * ModuleAsstDataProducer supports five types of files:
 * </p>
 * <ul>
 * <li>Asset files. These correspond to an AssetFileFormat, and provide the core data for an asset. They are
 * expected to be found under the /assets/<b>folderName</b> directory of modules.</li>
 * <li>Asset Supplementary files. These correspond to an AssetAlterationFileFormat, and provide additional data for an
 * asset. They are expected to be found under the /assets/<b>folderName</b> directory of modules. Supplementary formats
 * can be used by assets of any format - for instance a texture may support both png and jpg formats, and for either a
 * .info file could be provided with additional metadata.</li>
 * <li>Asset redirects. These are used to indicate a urn should be resolved to another urn. These are intended to support
 * assets being renamed or deleted. They are simple text containing the urn to redirect to, with a name corresponding to
 * a urn and a .redirect extension that contain the urn to use instead.
 * Like asset files, they are expected to be found under the /assets/<b>folderName</b> directory of modules.</li>
 * <li>Asset deltas. These are found under /deltas/<b>moduleName</b>/<b>folderName</b>, and provide changes to assets from
 * other modules. An AssetAlterationFileFormat is used to load them.</li>
 * <li>Asset overrides. These are found under /overrides/<b>moduleName</b>/<b>folderName</b>, and replace completely
 * the data of an asset from another module. All the asset formats and asset supplementary formats are used to load these.</li>
 * </ul>
 * <p>
 * When the data for an asset is requested, ModuleAssetDataProducer will return the data using all of the relevant files across
 * all modules.
 * </p>
 * <p>
 * ModuleAssetDataProducer also sets up watchers for any modules that are folders on the file system. This allows the file
 * system to be checked for any changed assets, and these assets reloaded as desired.
 * </p>
 *
 * @author Immortius
 */
@ThreadSafe
public class ModuleAssetDataProducer<U extends AssetData>
        implements AssetDataProducer<U>, AssetFileChangeSubscriber {

    /**
     * The name of the module directory that contains asset files.
     */
    public static final String ASSET_FOLDER = "assets";

    /**
     * The name of the module directory that contains overrides.
     */
    public static final String OVERRIDE_FOLDER = "overrides";

    /**
     * The name of the module directory that contains detlas.
     */
    public static final String DELTA_FOLDER = "deltas";

    /**
     * The extension for redirects.
     */
    public static final String REDIRECT_EXTENSION = "redirect";

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

    private final ImmutableList<String> folderNames;

    private final ModuleEnvironment moduleEnvironment;

    private final ImmutableList<AssetFileFormat<U>> assetFormats;
    private final ImmutableList<AssetAlterationFileFormat<U>> deltaFormats;
    private final ImmutableList<AssetAlterationFileFormat<U>> supplementFormats;

    private final Map<ResourceUrn, UnloadedAssetData<U>> unloadedAssetLookup = new MapMaker().concurrencyLevel(1)
            .makeMap();
    private final ImmutableMap<ResourceUrn, ResourceUrn> redirectMap;
    private final SetMultimap<Name, Name> resolutionMap = Multimaps
            .synchronizedSetMultimap(HashMultimap.<Name, Name>create());

    /**
     * Creates a ModuleAssetDataProducer
     *
     * @param moduleEnvironment   The module environment to load asset data from
     * @param assetFormats        The file formats supported for loading asset files
     * @param supplementalFormats The supplementary file formats supported when loading asset files
     * @param deltaFormats        The delta file formats supported when loading asset files
     * @param folderNames         The subfolders that contains files relevant to the asset data this producer loads
     */
    public ModuleAssetDataProducer(ModuleEnvironment moduleEnvironment, Collection<AssetFileFormat<U>> assetFormats,
            Collection<AssetAlterationFileFormat<U>> supplementalFormats,
            Collection<AssetAlterationFileFormat<U>> deltaFormats, String... folderNames) {
        this(moduleEnvironment, assetFormats, supplementalFormats, deltaFormats, Arrays.asList(folderNames));
    }

    /**
     * Creates a ModuleAssetDataProducer
     *
     * @param moduleEnvironment   The module environment to load asset data from
     * @param assetFormats        The file formats supported for loading asset files
     * @param supplementalFormats The supplementary file formats supported when loading asset files
     * @param deltaFormats        The delta file formats supported when loading asset files
     * @param folderNames         The subfolders that contains files relevant to the asset data this producer loads
     */
    public ModuleAssetDataProducer(ModuleEnvironment moduleEnvironment, Collection<AssetFileFormat<U>> assetFormats,
            Collection<AssetAlterationFileFormat<U>> supplementalFormats,
            Collection<AssetAlterationFileFormat<U>> deltaFormats, Collection<String> folderNames) {
        this.folderNames = ImmutableList.copyOf(folderNames);
        this.moduleEnvironment = moduleEnvironment;
        this.assetFormats = ImmutableList.copyOf(assetFormats);
        this.supplementFormats = ImmutableList.copyOf(supplementalFormats);
        this.deltaFormats = ImmutableList.copyOf(deltaFormats);

        scanForAssets();
        scanForOverrides();
        scanForDeltas();
        redirectMap = buildRedirectMap(scanModulesForRedirects());
    }

    /**
     * @return A list of the asset file formats supported
     */
    public ImmutableList<AssetFileFormat<U>> getAssetFormats() {
        return assetFormats;
    }

    /**
     * @return A list of the supplement file formats supported
     */
    public ImmutableList<AssetAlterationFileFormat<U>> getSupplementFormats() {
        return supplementFormats;
    }

    /**
     * @return A list of the delta file formats supported
     */
    public ImmutableList<AssetAlterationFileFormat<U>> getDeltaFormats() {
        return deltaFormats;
    }

    /**
     * @return The module environment that asset data is read from
     */
    public ModuleEnvironment getModuleEnvironment() {
        return moduleEnvironment;
    }

    @Override
    public Set<ResourceUrn> getAvailableAssetUrns() {
        return ImmutableSet.copyOf(unloadedAssetLookup.keySet());
    }

    @Override
    public Set<Name> getModulesProviding(Name resourceName) {
        return ImmutableSet.copyOf(resolutionMap.get(resourceName));
    }

    @Override
    public ResourceUrn redirect(ResourceUrn urn) {
        ResourceUrn redirectUrn = redirectMap.get(urn);
        if (redirectUrn != null) {
            return redirectUrn;
        }
        return urn;
    }

    @Override
    public Optional<U> getAssetData(ResourceUrn urn) throws IOException {
        if (urn.getFragmentName().isEmpty()) {
            UnloadedAssetData<U> source = unloadedAssetLookup.get(urn);
            if (source != null && source.isValid()) {
                return source.load();
            }
        }
        return Optional.empty();
    }

    private void scanForAssets() {
        for (Module module : moduleEnvironment.getModulesOrderedByDependencies()) {
            for (String folderName : folderNames) {
                Path rootPath = moduleEnvironment.getFileSystem().getPath(ModuleFileSystemProvider.ROOT,
                        module.getId().toString(), ASSET_FOLDER, folderName);
                if (Files.exists(rootPath)) {
                    scanLocationForAssets(module, folderName, rootPath, path -> module.getId());
                }
            }
        }
    }

    private void scanForOverrides() {
        for (Module module : moduleEnvironment.getModulesOrderedByDependencies()) {
            for (String folderName : folderNames) {
                Path rootPath = moduleEnvironment.getFileSystem().getPath(ModuleFileSystemProvider.ROOT,
                        module.getId().toString(), OVERRIDE_FOLDER);
                if (Files.exists(rootPath)) {
                    try {
                        Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
                            @Override
                            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                                    throws IOException {
                                return FileVisitResult.SKIP_SIBLINGS;
                            }

                            @Override
                            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                                    throws IOException {
                                if (dir.getNameCount() == rootPath.getNameCount() + 1) {
                                    Path overridePath = dir.resolve(folderName);
                                    if (Files.isDirectory(overridePath)) {
                                        scanLocationForAssets(module, folderName, overridePath,
                                                path -> new Name(path.getName(2).toString()));
                                    }
                                    return FileVisitResult.SKIP_SUBTREE;
                                }
                                return FileVisitResult.CONTINUE;
                            }

                        });
                    } catch (IOException e) {
                        logger.error("Failed to scan for override assets of '{}' in 'module://{}:{}", folderName,
                                module.getId(), rootPath, e);
                    }

                }
            }
        }
    }

    private void scanLocationForAssets(final Module origin, String folderName, Path rootPath,
            Function<Path, Name> moduleNameProvider) {
        try {
            Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Name module = moduleNameProvider.apply(file);
                    Optional<ResourceUrn> assetUrn = registerSource(module, file, origin.getId(), assetFormats,
                            UnloadedAssetData::addSource);
                    if (!assetUrn.isPresent()) {
                        registerSource(moduleNameProvider.apply(file), file, origin.getId(), supplementFormats,
                                UnloadedAssetData::addSupplementSource);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            logger.error("Failed to scan for assets of '{}' in 'module://{}:{}", folderName, origin.getId(),
                    rootPath, e);
        }
    }

    private void scanForDeltas() {
        for (final Module module : moduleEnvironment.getModulesOrderedByDependencies()) {
            for (String folderName : folderNames) {
                Path rootPath = moduleEnvironment.getFileSystem().getPath(ModuleFileSystemProvider.ROOT,
                        module.getId().toString(), DELTA_FOLDER);
                if (Files.exists(rootPath)) {
                    try {
                        Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() {
                            @Override
                            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                                    throws IOException {
                                registerAssetDelta(new Name(file.getName(2).toString()), file, module.getId());
                                return FileVisitResult.CONTINUE;
                            }
                        });
                    } catch (IOException e) {
                        logger.error("Failed to scan for asset deltas of '{}' in 'module://{}:{}", folderName,
                                module.getId(), rootPath, e);
                    }
                }
            }
        }
    }

    private Map<ResourceUrn, ResourceUrn> scanModulesForRedirects() {
        Map<ResourceUrn, ResourceUrn> rawRedirects = Maps.newLinkedHashMap();
        for (Module module : moduleEnvironment.getModulesOrderedByDependencies()) {
            for (String folderName : folderNames) {
                Path rootPath = moduleEnvironment.getFileSystem().getPath(ModuleFileSystemProvider.ROOT,
                        module.getId().toString(), ASSET_FOLDER, folderName);
                if (Files.exists(rootPath)) {
                    try {
                        for (Path file : FileScanning.findFilesInPath(rootPath, FileScanning.acceptAll(),
                                new FileExtensionPathMatcher(REDIRECT_EXTENSION))) {
                            processRedirectFile(file, module.getId(), rawRedirects);
                        }
                    } catch (IOException e) {
                        logger.error("Failed to scan module '{}' for assets", module.getId(), e);
                    }
                }
            }
        }
        return rawRedirects;
    }

    private ImmutableMap<ResourceUrn, ResourceUrn> buildRedirectMap(Map<ResourceUrn, ResourceUrn> rawRedirects) {
        ImmutableMap.Builder<ResourceUrn, ResourceUrn> builder = ImmutableMap.builder();
        for (Map.Entry<ResourceUrn, ResourceUrn> entry : rawRedirects.entrySet()) {
            ResourceUrn currentTarget = entry.getKey();
            ResourceUrn redirect = entry.getValue();
            while (redirect != null) {
                currentTarget = redirect;
                redirect = rawRedirects.get(currentTarget);
            }
            builder.put(entry.getKey(), currentTarget);
        }
        return builder.build();
    }

    private void processRedirectFile(Path file, Name moduleId, Map<ResourceUrn, ResourceUrn> rawRedirects) {
        Path filename = file.getFileName();
        if (filename != null) {
            Name assetName = new Name(com.google.common.io.Files.getNameWithoutExtension(filename.toString()));
            try (BufferedReader reader = Files.newBufferedReader(file, Charsets.UTF_8)) {
                List<String> contents = CharStreams.readLines(reader);
                if (contents.isEmpty()) {
                    logger.error("Failed to read redirect '{}:{}' - empty", moduleId, assetName);
                } else if (!ResourceUrn.isValid(contents.get(0))) {
                    logger.error("Failed to read redirect '{}:{}' - '{}' is not a valid urn", moduleId, assetName,
                            contents.get(0));
                } else {
                    rawRedirects.put(new ResourceUrn(moduleId, assetName), new ResourceUrn(contents.get(0)));
                    resolutionMap.put(assetName, moduleId);
                }
            } catch (IOException e) {
                logger.error("Failed to read redirect '{}:{}'", moduleId, assetName, e);
            }
        } else {
            logger.error("Missing file name for redirect");
        }
    }

    private <V extends FileFormat> Optional<ResourceUrn> registerSource(Name module, Path target,
            Name providingModule, Collection<V> formats, RegisterSourceHandler<U, V> sourceHandler) {
        Path filename = target.getFileName();
        if (filename == null) {
            logger.error("Missing filename for asset file");
            return Optional.empty();
        }
        for (V format : formats) {
            if (format.getFileMatcher().matches(target)) {
                try {
                    Name assetName = format.getAssetName(filename.toString());
                    ResourceUrn urn = new ResourceUrn(module, assetName);
                    UnloadedAssetData<U> existing = unloadedAssetLookup.get(urn);
                    if (existing != null) {
                        if (sourceHandler.registerSource(existing, providingModule, format, target)) {
                            return Optional.of(urn);
                        }
                    } else {
                        UnloadedAssetData<U> source = new UnloadedAssetData<>(urn, moduleEnvironment);
                        if (sourceHandler.registerSource(source, providingModule, format, target)) {
                            unloadedAssetLookup.put(urn, source);
                            resolutionMap.put(urn.getResourceName(), urn.getModuleName());
                            return Optional.of(urn);
                        }
                    }
                    return Optional.empty();
                } catch (InvalidAssetFilenameException e) {
                    logger.warn("Invalid name for asset - {}", filename);
                }
            }
        }
        return Optional.empty();
    }

    private Optional<ResourceUrn> registerAssetDelta(Name module, Path target, Name providingModule) {
        Path filename = target.getFileName();
        if (filename == null) {
            logger.error("Missing file name for asset delta for '{}'", target);
            return Optional.empty();
        }
        for (AssetAlterationFileFormat<U> format : deltaFormats) {
            if (format.getFileMatcher().matches(target)) {
                try {
                    Name assetName = format.getAssetName(filename.toString());
                    ResourceUrn urn = new ResourceUrn(module, assetName);
                    UnloadedAssetData<U> unloadedAssetData = unloadedAssetLookup.get(urn);
                    if (unloadedAssetData == null) {
                        logger.warn("Discovered delta for unknown asset '{}'", urn);
                        return Optional.empty();
                    }
                    if (unloadedAssetData.addDeltaSource(providingModule, format, target)) {
                        return Optional.of(urn);
                    }
                } catch (InvalidAssetFilenameException e) {
                    logger.error("Invalid file name '{}' for asset delta", target.getFileName(), e);
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> assetFileAdded(Path path, Name module, Name providingModule) {
        Optional<ResourceUrn> urn = registerSource(module, path, providingModule, assetFormats,
                UnloadedAssetData::addSource);
        if (!urn.isPresent()) {
            urn = registerSource(module, path, providingModule, supplementFormats,
                    UnloadedAssetData::addSupplementSource);
        }
        if (urn.isPresent() && unloadedAssetLookup.get(urn.get()).isValid()) {
            return urn;
        }
        return Optional.empty();
    }

    private Optional<ResourceUrn> getResourceUrn(Path target, Name module,
            Collection<? extends FileFormat> formats) {
        Path filename = target.getFileName();
        if (filename != null) {
            for (FileFormat fileFormat : formats) {
                if (fileFormat.getFileMatcher().matches(target)) {
                    try {
                        Name assetName = fileFormat.getAssetName(filename.toString());
                        return Optional.of(new ResourceUrn(module, assetName));
                    } catch (InvalidAssetFilenameException e) {
                        logger.debug("Modified file does not have a valid asset name - '{}'", filename);
                    }
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> assetFileModified(Path path, Name module, Name providingModule) {
        Optional<ResourceUrn> urn = getResourceUrn(path, module, assetFormats);
        if (!urn.isPresent()) {
            urn = getResourceUrn(path, module, supplementFormats);
        }
        if (urn.isPresent() && unloadedAssetLookup.get(urn.get()).isValid()) {
            return urn;
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> assetFileDeleted(Path path, Name module, Name providingModule) {
        Path filename = path.getFileName();
        if (filename != null) {
            for (AssetFileFormat<U> format : assetFormats) {
                if (format.getFileMatcher().matches(path)) {
                    try {
                        Name assetName = format.getAssetName(filename.toString());
                        ResourceUrn urn = new ResourceUrn(module, assetName);
                        UnloadedAssetData<U> existing = unloadedAssetLookup.get(urn);
                        if (existing != null) {
                            existing.removeSource(providingModule, format, path);
                            if (existing.isValid()) {
                                return Optional.of(urn);
                            }
                        }
                        return Optional.empty();
                    } catch (InvalidAssetFilenameException e) {
                        logger.debug("Deleted file does not have a valid file name - {}", path);
                    }
                }
            }
            for (AssetAlterationFileFormat<U> format : supplementFormats) {
                if (format.getFileMatcher().matches(path)) {
                    try {
                        Name assetName = format.getAssetName(filename.toString());
                        ResourceUrn urn = new ResourceUrn(module, assetName);
                        UnloadedAssetData<U> existing = unloadedAssetLookup.get(urn);
                        if (existing != null) {
                            existing.removeSupplementSource(providingModule, format, path);
                            if (existing.isValid()) {
                                return Optional.of(urn);
                            }
                        }
                        return Optional.empty();
                    } catch (InvalidAssetFilenameException e) {
                        logger.debug("Deleted file does not have a valid file name - {}", path);
                    }
                }
            }
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> deltaFileAdded(Path path, Name module, Name providingModule) {
        Optional<ResourceUrn> urn = registerAssetDelta(module, path, providingModule);
        if (urn.isPresent() && unloadedAssetLookup.get(urn.get()).isValid()) {
            return urn;
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> deltaFileModified(Path path, Name module, Name providingModule) {
        Optional<ResourceUrn> urn = getResourceUrn(path, module, deltaFormats);
        if (urn.isPresent()) {
            if (unloadedAssetLookup.get(urn.get()).isValid()) {
                return urn;
            }
        }
        return Optional.empty();
    }

    @Override
    public Optional<ResourceUrn> deltaFileDeleted(Path path, Name module, Name providingModule) {
        Path filename = path.getFileName();
        if (filename == null) {
            logger.error("Missing filename for deleted file");
            return Optional.empty();
        }
        for (AssetAlterationFileFormat<U> format : deltaFormats) {
            if (format.getFileMatcher().matches(path)) {
                try {
                    Name assetName = format.getAssetName(filename.toString());
                    ResourceUrn urn = new ResourceUrn(module, assetName);
                    UnloadedAssetData<U> existing = unloadedAssetLookup.get(urn);
                    if (existing != null) {
                        existing.removeDeltaSource(providingModule, format, path);
                        if (existing.isValid()) {
                            return Optional.of(urn);
                        }
                    }
                    return Optional.empty();
                } catch (InvalidAssetFilenameException e) {
                    logger.debug("Deleted file does not have a valid file name - {}", path);
                }
            }
        }
        return Optional.empty();
    }

    /**
     * Interface for registering a source. Allows the same outer logic to be used for registering different types of asset sources.
     *
     * @param <T>
     * @param <U>
     */
    private interface RegisterSourceHandler<T extends AssetData, U extends FileFormat> {
        boolean registerSource(UnloadedAssetData<T> source, Name providingModule, U format, Path input);
    }

}