/**
* Copyright (C) 2013-2014 Olaf Lessenich
* Copyright (C) 2014-2015 University of Passau, Germany
*
* This library 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 2.1 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Contributors:
* Olaf Lessenich <lessenic@fim.uni-passau.de>
* Georg Seibt <seibt@fim.uni-passau.de>
*/
package de.fosd.jdime.artifact.file;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.activation.MimetypesFileTypeMap;
import de.fosd.jdime.artifact.Artifact;
import de.fosd.jdime.artifact.ArtifactList;
import de.fosd.jdime.artifact.ast.ASTNodeArtifact;
import de.fosd.jdime.config.merge.MergeContext;
import de.fosd.jdime.config.merge.MergeScenario;
import de.fosd.jdime.config.merge.Revision;
import de.fosd.jdime.execption.AbortException;
import de.fosd.jdime.execption.NotYetImplementedException;
import de.fosd.jdime.merge.Merge;
import de.fosd.jdime.operations.MergeOperation;
import de.fosd.jdime.stats.ElementStatistics;
import de.fosd.jdime.stats.KeyEnums;
import de.fosd.jdime.stats.MergeScenarioStatistics;
import de.fosd.jdime.stats.StatisticsInterface;
import de.fosd.jdime.strategy.LinebasedStrategy;
import de.fosd.jdime.strategy.MergeStrategy;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.comparator.CompositeFileComparator;
import static java.util.logging.Level.SEVERE;
import static org.apache.commons.io.comparator.DirectoryFileComparator.DIRECTORY_COMPARATOR;
import static org.apache.commons.io.comparator.NameFileComparator.NAME_COMPARATOR;
/**
* This class represents an artifact of a program.
*
* @author Olaf Lessenich
*/
public class FileArtifact extends Artifact<FileArtifact> {
private static final Logger LOG = Logger.getLogger(FileArtifact.class.getCanonicalName());
/**
* The expected MIME content type for java source files.
*/
private static final String MIME_JAVA_SOURCE = "text/x-java";
/**
* Used for determining the content type of this <code>FileArtifact</code> if
* {@link Files#probeContentType(java.nio.file.Path)} fails.
*/
private static final MimetypesFileTypeMap mimeMap;
static {
mimeMap = new MimetypesFileTypeMap();
mimeMap.addMimeTypes(MIME_JAVA_SOURCE + " java");
}
/**
* A <code>Comparator</code> to compare <code>FileArtifact</code>s by their <code>File</code>s. It considers
* all directories smaller than files and otherwise compares by the file name.
*/
private static final Comparator<FileArtifact> comp = new Comparator<FileArtifact>() {
@SuppressWarnings("unchecked")
private Comparator<File> c = new CompositeFileComparator(DIRECTORY_COMPARATOR, NAME_COMPARATOR);
@Override
public int compare(FileArtifact o1, FileArtifact o2) {
return c.compare(o1.getFile(), o2.getFile());
}
};
/**
* File in which the artifact is stored.
*/
private File file;
/**
* Constructs a new <code>FileArtifact</code> representing the given <code>File</code>.
* If <code>file</code> is a directory then <code>FileArtifact</code>s representing its contents will be added
* as children to this <code>FileArtifact</code>.
*
* @param revision
* the <code>Revision</code> the artifact belongs to
* @param file
* the <code>File</code> in which the artifact is stored
* @throws FileNotFoundException
* if <code>file</code> does not exist and <code>create</code> is <code>false</code>
* @throws IOException
* never thrown by this constructor
*/
public FileArtifact(Revision revision, File file) throws IOException {
this(revision, file, false, false);
}
/**
* Constructs a new <code>FileArtifact</code> representing the given <code>File</code>.
* If <code>file</code> is a directory then <code>FileArtifact</code>s representing its contents will be added
* as children to this <code>FileArtifact</code>.
*
* @param revision
* the <code>Revision</code> the artifact belongs to
* @param file
* the <code>File</code> in which the artifact is stored
* @param create
* whether to create that <code>file</code> if it does not exist
* @param createFile
* whether to create a file (instead of a directory), ignored if <code>create</code>
* is <code>false</code>
* @throws FileNotFoundException
* if <code>file</code> does not exist and <code>create</code> is <code>false</code>
* @throws IOException
* if <code>createNonExistent</code> is <code>false</code> and <code>file</code> does not exist according to
* {@link java.io.File#exists()}, or if <code>createNonExistent</code> is <code>true</code> but
* <code>file</code> cannot be created.
*/
public FileArtifact(Revision revision, File file, boolean create, boolean createFile) throws IOException {
this(revision, new AtomicInteger(0)::getAndIncrement, file, create, createFile);
}
/**
* Constructs a new <code>FileArtifact</code> representing the given <code>File</code>.
* If <code>file</code> is a directory then <code>FileArtifact</code>s representing its contents will be added
* as children to this <code>FileArtifact</code>.
*
* @param revision
* the <code>Revision</code> the artifact belongs to
* @param number
* supplies first the number for this artifact and then in DFS order the number for its children
* @param file
* the <code>File</code> in which the artifact is stored
* @param create
* whether to create that <code>file</code> if it does not exist
* @param createFile
* whether to create a file (instead of a directory), ignored if <code>create</code>
* is <code>false</code>
* @throws FileNotFoundException
* if <code>file</code> does not exist and <code>create</code> is <code>false</code>
* @throws IOException
* if <code>createNonExistent</code> is <code>false</code> and <code>file</code> does not exist according to
* {@link java.io.File#exists()}, or if <code>createNonExistent</code> is <code>true</code> but
* <code>file</code> cannot be created.
*/
private FileArtifact(Revision revision, Supplier<Integer> number, File file, boolean create, boolean createFile) throws IOException {
super(revision, number.get());
if (!file.exists()) {
if (create) {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
boolean createdParents = file.getParentFile().mkdirs();
LOG.finest(() -> "Had to create parent directories: " + createdParents);
}
if (createFile) {
if (file.createNewFile()) {
LOG.finest(() -> "Created file" + file);
} else {
throw new IOException("Could not create " + file);
}
} else {
if (file.mkdir()) {
LOG.finest(() -> "Created directory " + file);
} else {
throw new IOException("Could not create directory " + file);
}
}
} else {
throw new FileNotFoundException("File not found: " + file.getAbsolutePath());
}
}
this.file = file;
if (isDirectory()) {
children = new ArtifactList<>();
children.addAll(getDirContent(number));
Collections.sort(children, comp);
} else {
children = null;
}
}
@Override
public FileArtifact addChild(FileArtifact child) {
if (!exists()) {
String msg = String.format("FileArtifact '%s' does not exist. Can not add '%s' as a child.", this, child);
throw new IllegalStateException(msg);
}
if (!isDirectory()) {
String msg = String.format("FileArtifact '%s' does not represent a directory. Can not add '%s' as a child.", this, child);
throw new IllegalStateException(msg);
}
File copy = new File(file, child.getFile().getName());
try {
if (child.isFile()) {
LOG.fine(() -> String.format("Copying file %s to directory %s", child, this));
FileUtils.copyFile(child.file, copy);
} else if (child.isDirectory()) {
LOG.fine(() -> String.format("Copying directory %s to directory %s", child, this));
FileUtils.copyDirectory(child.file, copy);
}
} catch (IOException e) {
LOG.log(SEVERE, e, () -> String.format("Failed to add a FileArtifact representing '%s' as a child to '%s'.", child, this));
throw new RuntimeException(e);
}
FileArtifact added;
try {
added = new FileArtifact(getRevision(), copy);
} catch (IOException e) {
// the constructor can not throw IOException
throw new RuntimeException(e);
}
children.add(added);
Collections.sort(children, comp);
return added;
}
@Override
public FileArtifact clone() {
LOG.finest(() -> "CLONE: " + this);
try {
return new FileArtifact(getRevision(), file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public final FileArtifact createEmptyArtifact(Revision revision) {
try {
File temp = Files.createTempFile(null, null).toFile();
temp.deleteOnExit();
FileArtifact emptyFile = new FileArtifact(revision, temp);
LOG.finest(() -> "Artifact is a dummy artifact. Using temporary file: " + emptyFile.getFullPath());
return emptyFile;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public String prettyPrint() {
return getContent();
}
@Override
public final boolean exists() {
return file.exists();
}
@Override
public void deleteChildren() {
LOG.finest(() -> this + ".deleteChildren()");
if (exists()) {
if (isDirectory()) {
for (FileArtifact child : children) {
child.remove();
}
} else {
remove();
try {
if (!file.createNewFile()) {
throw new IOException("File#createNewFile returned false.");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* Removes all <code>FileArtifact</code>s under this one representing files that are not Java source code files
* (according to {@link #isJavaFile()}) or directories that do not contain (possibly indirectly) any java source
* code files.
*/
public void filterNonJavaFiles() {
if (isDirectory()) {
children.stream().filter(FileArtifact::isDirectory).forEach(FileArtifact::filterNonJavaFiles);
LOG.fine(() -> "Filtering out the children not representing java source code files from " + this);
children.removeIf(c -> (c.isFile() && !c.isJavaFile()) || (c.isDirectory() && c.getChildren().isEmpty()));
}
}
/**
* Returns whether this <code>FileArtifact</code> (probably) represents a Java source code file.
*
* @return true iff this <code>FileArtifact</code> likely represents a Java source code file
*/
public boolean isJavaFile() {
return isFile() && MIME_JAVA_SOURCE.equals(getContentType());
}
/**
* Returns the MIME content type of the <code>File</code> in which this <code>FileArtifact</code> is stored.
* If the content type can not be determined <code>null</code> will be returned.
*
* @return the MIME content type
*/
private String getContentType() {
assert (exists());
String mimeType = null;
try {
mimeType = Files.probeContentType(file.toPath());
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Could not probe content type of " + file);
}
if (mimeType == null) {
// returns application/octet-stream if the type can not be determined
mimeType = mimeMap.getContentType(file);
if ("application/octet-stream".equals(mimeType)) {
mimeType = null;
}
}
return mimeType;
}
/**
* Returns newly allocated <code>FileArtifacts</code> representing the files contained in the directory represented
* by this <code>FileArtifact</code>. If this <code>FileArtifact</code> does not represent a directory, an empty
* list is returned.
*
* @param number
* the number <code>Supplier</code> to be passed to the new <code>FileArtifact</code>s
* @return <code>FileArtifacts</code> representing the children of this directory
*/
private List<FileArtifact> getDirContent(Supplier<Integer> number) {
File[] files = file.listFiles();
if (files == null) {
LOG.warning(() -> String.format("Tried to get the directory contents of %s which is not a directory.", this));
return Collections.emptyList();
} else if (files.length == 0) {
return Collections.emptyList();
}
List<FileArtifact> artifacts = new ArrayList<>(files.length);
for (File f : files) {
try {
FileArtifact child = new FileArtifact(getRevision(), number, f, false, false);
child.setParent(this);
artifacts.add(child);
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Could not create the FileArtifact for " + f);
}
}
return artifacts;
}
/**
* Returns the encapsulated file.
*
* @return file
*/
public final File getFile() {
return file;
}
private List<FileArtifact> getJavaFiles() {
return getJavaFiles(new ArtifactList<>());
}
private List<FileArtifact> getJavaFiles(List<FileArtifact> list) {
if (isJavaFile()) {
list.add(this);
} else if (isDirectory()) {
children.forEach(c -> c.getJavaFiles(list));
}
return list;
}
/**
* Returns the absolute path of this artifact.
*
* @return absolute part of the artifact
*/
public final String getFullPath() {
return file.getAbsolutePath();
}
@Override
public final String getId() {
return getRevision() + ":" + getPath();
}
/**
* Returns the path of this artifact.
*
* @return path of the artifact
*/
public final String getPath() {
return file.getPath();
}
/**
* Returns a reader that can be used to retrieve the content of the
* artifact.
*
* @return Reader
* @throws FileNotFoundException
* If the artifact is a file which is not found
*/
public final BufferedReader getReader() throws FileNotFoundException {
if (isFile()) {
return new BufferedReader(new FileReader(file));
} else {
throw new NotYetImplementedException();
}
}
/**
* Returns the list of (relative) filenames contained in this directory.
*
* @return list of relative filenames
*/
public final List<String> getRelativeDirContent() {
assert (isDirectory());
return Arrays.asList(file.list());
}
@Override
public KeyEnums.Type getType() {
return isDirectory() ? KeyEnums.Type.DIRECTORY : KeyEnums.Type.FILE;
}
@Override
public KeyEnums.Level getLevel() {
return KeyEnums.Level.NONE;
}
@Override
public void addOpStatistics(MergeScenarioStatistics mScenarioStatistics, MergeContext mergeContext) {
mScenarioStatistics.getTypeStatistics(null, getType()).incrementNumAdded();
if (!(mergeContext.getMergeStrategy() instanceof LinebasedStrategy)) {
forAllJavaFiles(astNodeArtifact -> {
mScenarioStatistics.add(StatisticsInterface.getASTStatistics(astNodeArtifact, null));
});
}
}
@Override
public void deleteOpStatistics(MergeScenarioStatistics mScenarioStatistics, MergeContext mergeContext) {
mScenarioStatistics.getTypeStatistics(null, getType()).incrementNumDeleted();
if (!(mergeContext.getMergeStrategy() instanceof LinebasedStrategy)) {
forAllJavaFiles(astNodeArtifact -> {
MergeScenarioStatistics delStats = StatisticsInterface.getASTStatistics(astNodeArtifact, null);
Map<Revision, Map<KeyEnums.Level, ElementStatistics>> lStats = delStats.getLevelStatistics();
Map<Revision, Map<KeyEnums.Type, ElementStatistics>> tStats = delStats.getTypeStatistics();
for (Map.Entry<Revision, Map<KeyEnums.Level, ElementStatistics>> entry : lStats.entrySet()) {
for (Map.Entry<KeyEnums.Level, ElementStatistics> sEntry : entry.getValue().entrySet()) {
ElementStatistics eStats = sEntry.getValue();
eStats.setNumDeleted(eStats.getNumAdded());
eStats.setNumAdded(0);
}
}
for (Map.Entry<Revision, Map<KeyEnums.Type, ElementStatistics>> entry : tStats.entrySet()) {
for (Map.Entry<KeyEnums.Type, ElementStatistics> sEntry : entry.getValue().entrySet()) {
ElementStatistics eStats = sEntry.getValue();
eStats.setNumDeleted(eStats.getNumAdded());
eStats.setNumAdded(0);
}
}
mScenarioStatistics.add(delStats);
});
}
}
@Override
public void mergeOpStatistics(MergeScenarioStatistics mScenarioStatistics, MergeContext mergeContext) {
mScenarioStatistics.getTypeStatistics(null, getType()).incrementNumMerged();
}
/**
* Uses {@link #getJavaFiles()} and applies the given <code>Consumer</code> to every resulting
* <code>FileArtifact</code> after it being parsed to an <code>ASTNodeArtifact</code>. If an
* <code>IOException</code> occurs getting the files the method will immediately return. If an
* <code>IOException</code> occurs parsing a file to an <code>ASTNodeArtifact</code> it will be skipped.
*
* @param cons
* the <code>Consumer</code> to apply
*/
private void forAllJavaFiles(Consumer<ASTNodeArtifact> cons) {
for (FileArtifact child : getJavaFiles()) {
ASTNodeArtifact childAST;
try {
childAST = new ASTNodeArtifact(child);
} catch (RuntimeException e) {
LOG.log(Level.WARNING, e, () -> {
String format = "Could not construct an ASTNodeArtifact from %s. No statistics will be collected for it.";
return String.format(format, child);
});
continue;
}
cons.accept(childAST);
}
}
@Override
public Optional<Supplier<String>> getUniqueLabel() {
return Optional.of(() -> file.getName());
}
/**
* Returns true if artifact is a directory.
*
* @return true if artifact is a directory
*/
public final boolean isDirectory() {
return file.isDirectory();
}
/**
* Returns true if the artifact is empty.
*
* @return true if the artifact is empty
*/
@Override
public final boolean isEmpty() {
assert (exists());
if (isDirectory()) {
return file.listFiles().length == 0;
} else {
return FileUtils.sizeOf(file) == 0;
}
}
/**
* Returns true if artifact is a normal file.
*
* @return true if artifact is a normal file
*/
public final boolean isFile() {
return file.isFile();
}
@Override
public final boolean isLeaf() {
return !file.isDirectory();
}
@Override
public final boolean isOrdered() {
return false;
}
@Override
public final boolean matches(final FileArtifact other) {
if (isDirectory() && isRoot() && other.isDirectory() && other.isRoot()) {
LOG.fine(() -> String.format("%s and %s are toplevel directories.", this, other));
LOG.fine("We assume a match here and continue to merge the contained files and directories.");
return true;
}
return this.toString().equals(other.toString());
}
@Override
public void merge(MergeOperation<FileArtifact> operation, MergeContext context) {
Objects.requireNonNull(operation, "operation must not be null!");
Objects.requireNonNull(context, "context must not be null!");
if (!exists()) {
String className = getClass().getSimpleName();
String filePath = file.getAbsolutePath();
String message = String.format("Trying to merge %s whose file %s does not exist.", className, filePath);
throw new RuntimeException(message);
}
if (isDirectory()) {
Merge<FileArtifact> merge = new Merge<>();
if (context.hasStatistics()) {
context.getStatistics().setCurrentFileMergeScenario(operation.getMergeScenario());
}
LOG.finest(() -> "Merging directories " + operation.getMergeScenario());
merge.merge(operation, context);
} else {
MergeStrategy<FileArtifact> strategy = context.getMergeStrategy();
MergeScenario<FileArtifact> scenario = operation.getMergeScenario();
if (!isJavaFile()) {
LOG.fine(() -> "Skipping non-java file " + this);
return;
}
if (context.hasStatistics()) {
context.getStatistics().setCurrentFileMergeScenario(scenario);
}
try {
strategy.merge(operation, context);
if (!context.isQuiet() && context.hasOutput()) {
System.out.print(context.getStdIn());
}
} catch (AbortException e) {
throw e; // AbortExceptions must always cause the merge to be aborted
} catch (RuntimeException e) {
context.addCrash(scenario, e);
LOG.log(SEVERE, e, () -> {
String ls = System.lineSeparator();
String scStr = operation.getMergeScenario().toString(ls, true);
return String.format("Exception while merging%n%s", scStr);
});
if (context.isExitOnError()) {
throw new AbortException(e);
} else {
if (!context.isKeepGoing() && !(strategy instanceof LinebasedStrategy)) {
LOG.severe(() -> "Falling back to line based strategy.");
context.setMergeStrategy(MergeStrategy.parse(MergeStrategy.LINEBASED));
context.resetStreams();
merge(operation, context);
} else {
LOG.severe(() -> "Skipping " + scenario);
}
}
}
context.resetStreams();
}
}
/**
* Removes the artifact's file.
*/
public void remove() {
if (!exists()) {
return;
}
try {
if (isDirectory()) {
LOG.fine(() -> "Deleting directory recursively: " + file);
FileUtils.forceDelete(file);
} else if (isFile()) {
LOG.fine(() -> "Deleting file: " + file);
FileUtils.forceDelete(file);
} else {
throw new UnsupportedOperationException("Only files and directories can be removed at the moment");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public final String toString() {
return file.getName();
}
/**
* Writes the given <code>String</code> to this <code>FileArtifact</code>.
*
* @param str the <code>String</code> to write
*/
public void write(String str) {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
try {
FileUtils.forceMkdir(file.getParentFile());
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Could not create the parent folder of " + file);
}
}
try (FileWriter writer = new FileWriter(file)) {
writer.write(str);
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Could not write to " + this);
}
}
@Override
public FileArtifact createConflictArtifact(FileArtifact left, FileArtifact right) {
throw new NotYetImplementedException();
}
@Override
public FileArtifact createChoiceArtifact(String condition, FileArtifact artifact) {
throw new NotYetImplementedException();
}
public final String getContent() {
try {
return FileUtils.readFileToString(file, StandardCharsets.UTF_8);
} catch (IOException e) {
LOG.log(Level.WARNING, e, () -> "Could not read the contents of " + this);
return "";
}
}
}