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