de.monticore.io.paths.IterablePath.java Source code

Java tutorial

Introduction

Here is the source code for de.monticore.io.paths.IterablePath.java

Source

/*
 * ******************************************************************************
 * MontiCore Language Workbench
 * Copyright (c) 2015, MontiCore, All rights reserved.
 *
 * This project is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this project. If not, see <http://www.gnu.org/licenses/>.
 * ******************************************************************************
 */

package de.monticore.io.paths;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.io.FilenameUtils;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import de.se_rwth.commons.logging.Log;

/**
 * An IterablePath is specified as a list of directories and/or explicit files and a set of file
 * extensions. It then provides iteration and checking for existence of qualified relative paths
 * which are supposed to be contained in the IterablePath. This functionality is in some ways
 * similar to the Java classpath mechanism. The IterablePath therefore abstracts away from the
 * actual location of a file.<br>
 * <br>
 * For instance, if an IterablePath was created with a directory <b>"src/test/resources/"</b> which
 * contains a file <b>"src/test/resources/a/AFile.txt"</b> and <b>"txt"</b> was specified as a
 * desired extension, then the path <b>"a/File.txt"</b> will be contained in the resolved elements
 * and accessible via the returned iterator ({@link #get()}). <br>
 * <br>
 * <b>Note that the order of the underlying path entries matter.</b> The first found resolved entry
 * for a qualified relative path is taken thus potentially hiding later matches (possible conflicts
 * are logged with level DEBUG).
 * 
 * @author (last commit) $Author$
 * @version $Revision$, $Date$
 */
public final class IterablePath {

    /* Singleton empty iterable path. */
    static final IterablePath EMPTY = new IterablePath(Collections.emptyList(), Collections.emptyMap());

    /**
     * A singleton {@link IterablePath} denoting an empty {@link IterablePath}.
     * 
     * @return
     */
    public static IterablePath empty() {
        return EMPTY;
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied input {@link File} . The file
     * extension of the input {@link File} must be one of the supplied file extensions otherwise the
     * resulting {@link IterablePath} will be empty.
     * 
     * @param file
     * @param extensions
     * @return
     */
    public static IterablePath from(File file, Set<String> extensions) {
        return from(Lists.newArrayList(file.getAbsoluteFile()), extensions);
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied input {@link File} . The file
     * extension of the input {@link File} must be equal to the supplied file extension otherwise the
     * resulting {@link IterablePath} will be empty.
     * 
     * @param file
     * @param extension
     * @return
     */
    public static IterablePath from(File file, String extension) {
        return from(file, Sets.newLinkedHashSet(Arrays.asList(extension)));
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied list of {@link File}s containing all
     * files with the specified extensions. Note: an empty set of extensions will yield an empty
     * {@link IterablePath}.
     * 
     * @param files
     * @param extensions
     * @return
     */
    public static IterablePath from(List<File> files, Set<String> extensions) {
        List<Path> sourcePaths = files.stream().map(file -> file.toPath()).collect(Collectors.toList());
        return fromPaths(sourcePaths, extensions);
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied list of {@link File}s containing all
     * files with the specified extension. Note: an empty extension will yield an empty
     * {@link IterablePath}.
     * 
     * @param files
     * @param extension
     * @return
     */
    public static IterablePath from(List<File> files, String extension) {
        return from(files, Sets.newLinkedHashSet(Arrays.asList(extension)));
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied set of {@link Path}s containing all
     * files with the specified extensions. Note: an empty set of extensions will yield an empty
     * {@link IterablePath}.
     * 
     * @param paths
     * @param extensions
     * @return
     */
    public static IterablePath fromPaths(List<Path> paths, Set<String> extensions) {
        Map<Path, Path> pMap = new LinkedHashMap<>();
        for (Path path : paths) {
            List<Path> entries = walkFileTree(path).filter(getExtensionsPredicate(extensions))
                    .collect(Collectors.toList());
            for (Path entry : entries) {
                Path key = path.relativize(entry);
                if (key.toString().isEmpty()) {
                    key = entry;
                }
                if (pMap.get(key) != null) {
                    Log.debug("The qualified path " + key + " appears multiple times.",
                            IterablePath.class.getName());
                } else {
                    pMap.put(key, entry);
                }
            }
        }
        return new IterablePath(paths, pMap);
    }

    /**
     * Creates a new {@link IterablePath} based on the supplied set of {@link Path}s containing all
     * files with the specified extension. Note: an empty extension will yield an empty
     * {@link IterablePath}.
     * 
     * @param paths
     * @param extension
     * @return
     */
    public static IterablePath fromPaths(List<Path> paths, String extension) {
        return fromPaths(paths, Sets.newLinkedHashSet(Arrays.asList(extension)));
    }

    /**
     * Encapsulates creation of a {@link Stream} of {@link Path}s for a given start {@link Path}.
     * 
     * @param path
     * @return
     */
    protected static Stream<Path> walkFileTree(Path path) {
        if (path.toFile().exists()) {
            try {
                return Files.walk(path);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            Log.warn("0xA4074 The supplied path " + path.toString() + " does not exist.");
            return Stream.empty();
        }
    }

    /**
     * Creates a {@link Predicate} for (e.g.) filtering {@link Path} elements by the specified set of
     * file extensions.
     * 
     * @param extensions
     * @return
     */
    protected static Predicate<Path> getExtensionsPredicate(Set<String> extensions) {
        return path -> extensions.contains(FilenameUtils.getExtension(path.toString()));
    }

    /**
     * The paths used to initialize this iterable path.
     */
    final List<Path> paths;

    /**
     * The underlying map of qualified paths and their resolved paths.
     */
    final Map<Path, Path> pathMap;

    /**
     * Constructor for de.monticore.io.paths.IterablePath.
     * 
     * @param paths
     * @param pathMap
     */
    protected IterablePath(List<Path> paths, Map<Path, Path> pathMap) {
        if (paths == null) {
            throw new IllegalArgumentException("0xA4069 Path list must not be null.");
        }
        if (pathMap == null) {
            throw new IllegalArgumentException("0xA4068 Path map must not be null.");
        }
        this.paths = paths;
        this.pathMap = pathMap;
    }

    /**
     * Returns the actual list of paths underlying this iterable path, i.e., the list of paths used to
     * create this iterable path. The returned list is unmodifieable and may not be used to alter this
     * iterable path.
     * 
     * @return the list of paths used to create this iterable path
     */
    public List<Path> getPaths() {
        return Collections.unmodifiableList(this.paths);
    }

    /**
     * Returns an unmodifiable iterator over the resolved qualified elements of this
     * {@link IterablePath}. For instance, if this IterablePath was created with a directory
     * <b>"src/test/resources/"</b> which contains a file <b>"src/test/resources/a/AFile.txt"</b> and
     * <b>"txt"</b> was specified as a desired extension, then the path <b>"a/File.txt"</b> will be
     * contained in the resolved qualified elements and accessible via the returned iterator.
     * 
     * @return an iterator over all resolved qualified paths, i.e., qualified relative paths that are
     * contained in the specified path entries and that match the desired file extension(s)
     */
    public Iterator<Path> get() {
        return this.pathMap.keySet().stream().sorted().iterator();
    }

    /**
     * Returns an unmodifiable iterator over the resolved elements of this {@link IterablePath}. For
     * instance, if this IterablePath was created with a directory <b>"src/test/resources/"</b> which
     * contains a file <b>"src/test/resources/a/AFile.txt"</b> and <b>"txt"</b> was specified as a
     * desired extension, then the path <b>"src/test/resources/a/AFile.txt"</b> will be contained in
     * the resolved elements and accessible via the returned iterator.
     * 
     * @return an iterator over all resolved paths, i.e., paths that are contained in the specified
     * path entries and that match the desired file extension(s)
     */
    public Iterator<Path> getResolvedPaths() {
        return this.pathMap.values().stream().sorted().iterator();
    }

    /**
     * Checks whether a given qualified path exists in this IterablePath. Here, qualified means that a
     * file exists in one of the directories making up this IterablePath. The qualified path does not
     * require knowledge about where exactly the underlying file is located on the file system (or
     * project directory layout). It only suffices to know that a required file is qualified as
     * <b>a/AFile.txt</b>. The actually resolved path to a file that exists in this IterablePath can
     * be obtained by {@link IterablePath#getResolvedPath(Path)}. An (unmodifiable) iterator over all
     * qualified paths contained in this IterablePath can be obtained by {@link IterablePath#get()}.
     * 
     * @param path
     * @return
     */
    public boolean exists(Path path) {
        return this.pathMap.containsKey(path);
    }

    /**
     * Returns the actually resolved path of a qualified path. For instance, if this IterablePath was
     * created with the directories <b>"src/test/resources/1/"</b> and <b>"src/test/resources/1/"</b>
     * (in that order) where both contain a file <b>a/AFile.txt"</b> and <b>"txt"</b> was specified as
     * a desired extension, then the qualified path <b>"a/File.txt"</b> will yield the resolved path
     * <b>"src/test/resources/1/a/AFile.txt"</b>. This is because the respective directory was
     * specified first, thus its entries hide possible other matches in later directories.
     * 
     * @param path qualified path
     * @return the first resolved path matching the qualified path (the order of path directories
     * matters here).
     */
    public Optional<Path> getResolvedPath(Path path) {
        return Optional.ofNullable(this.pathMap.get(path));
    }

    @Override
    public String toString() {
        if (this.pathMap.isEmpty()) {
            return "[empty iterable path]";
        }
        String result = "[";
        result = result
                + this.pathMap.values().stream().sorted().map(Path::toString).collect(Collectors.joining(", "));
        return result + "]";
    }

}