de.fosd.jdime.artifact.file.FileArtifact.java Source code

Java tutorial

Introduction

Here is the source code for de.fosd.jdime.artifact.file.FileArtifact.java

Source

/**
 * Copyright (C) 2013-2014 Olaf Lessenich
 * Copyright (C) 2014-2017 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 javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.nio.file.Files;
import java.util.ArrayList;
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.IntFunction;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.IntStream;

import de.fosd.jdime.artifact.Artifact;
import de.fosd.jdime.artifact.ArtifactList;
import de.fosd.jdime.artifact.Artifacts;
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.config.merge.Revision.SuccessiveNameSupplier;
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 de.fosd.jdime.util.parser.Content;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.comparator.CompositeFileComparator;

import static de.fosd.jdime.stats.MergeScenarioStatus.FAILED;
import static java.nio.charset.StandardCharsets.UTF_8;
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());
        }
    };

    /**
     * The type of virtual {@link File} to be represented by a {@link FileArtifact}.
     */
    public enum FileType {
        FILE, DIR;
    }

    /**
     * A {@link Supplier} used for generating names for virtual {@link FileArtifact FileArtifacts}.
     */
    private static final SuccessiveNameSupplier virtualNameSupplier = new SuccessiveNameSupplier();

    /**
     * The type of file this {@link FileArtifact} represents.
     */
    private final FileType type;

    /**
     * The original existing {@link File} this {@link FileArtifact} represents or {@code null} if this
     * {@link FileArtifact} is virtual.
     */
    private final File original;

    /**
     * The current {@link File} this {@link FileArtifact} represents.
     */
    private File file;

    /**
     * The content of this {@link FileArtifact}. The content will be retrieved from the {@link #original} {@link File}
     * and written back to the {@link #file} after the merge.
     */
    private String content;

    /**
     * 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 IllegalArgumentException
     *         if {@code file} does not exist
     */
    public FileArtifact(Revision revision, File file) {
        this(revision, new AtomicInteger(0)::getAndIncrement, file, true);
    }

    /**
     * Constructs a new <code>FileArtifact</code> representing the given <code>File</code>.
     *
     * @param revision
     *         the <code>Revision</code> the artifact belongs to
     * @param file
     *         the <code>File</code> in which the artifact is stored
     * @param recursive
     *         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>.
     * @throws IllegalArgumentException
     *         if {@code file} does not exist
     */
    public FileArtifact(Revision revision, File file, boolean recursive) {
        this(revision, new AtomicInteger(0)::getAndIncrement, file, recursive);
    }

    /**
     * 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 recursive
     *         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>.
     * @throws IllegalArgumentException
     *         if {@code file} does not exist
     */
    private FileArtifact(Revision revision, Supplier<Integer> number, File file, boolean recursive) {
        super(revision, number.get());

        if (!file.exists()) {
            throw new IllegalArgumentException("File '" + file + "' does not exist.");
        }

        if (file.isFile()) {
            this.type = FileType.FILE;
        } else if (file.isDirectory()) {
            this.type = FileType.DIR;
        } else {
            throw new IllegalArgumentException("File '" + file + "' is not a normal file or directory.");
        }

        this.original = file;
        this.file = file;

        if (recursive && isDirectory()) {
            modifyChildren(children -> {
                children.addAll(getDirContent(number));
                children.sort(comp);
            });
        }
    }

    /**
     * Constructs a new virtual {@link FileArtifact} representing a non-existent {@link File} with a generated name. The
     * new {@link FileArtifact} will always have the number 0.
     *
     * @param revision
     *         the {@link Revision} the artifact belongs to
     * @param type
     *         the virtual type for the {@link FileArtifact}, must be one of {@link FileType#FILE} or
     *         {@link FileType#DIR}
     */
    public FileArtifact(Revision revision, FileType type) {
        super(revision, 0);

        this.type = type;
        this.original = null;

        File tempDir = FileUtils.getTempDirectory();
        IntFunction<File> toFile;

        if (type == FileType.DIR) {
            toFile = n -> {
                synchronized (virtualNameSupplier) {
                    return new File(tempDir, "VirtualDirectory_" + virtualNameSupplier.get());
                }
            };
        } else {
            toFile = n -> {
                synchronized (virtualNameSupplier) {
                    return new File(tempDir, "VirtualFile_" + virtualNameSupplier.get());
                }
            };
        }

        this.file = IntStream.range(0, Integer.MAX_VALUE).mapToObj(toFile).filter(f -> !f.exists()).findFirst()
                .orElseThrow(() -> new AbortException(
                        "Could not find an available file name for the virtual file or directory."));
    }

    /**
     * Copies the given {@link FileArtifact} detached from its tree.
     *
     * @param toCopy
     *         the {@link FileArtifact} to copy
     * @see #copy()
     */
    private FileArtifact(FileArtifact toCopy) {
        super(toCopy);

        this.type = toCopy.type;
        this.original = toCopy.original;
        this.file = toCopy.file;
        this.content = toCopy.content;
    }

    @Override
    protected FileArtifact self() {
        return this;
    }

    @Override
    public void addChild(FileArtifact child) {
        super.addChild(child);

        for (FileArtifact node : Artifacts.dfsIterable(child)) {
            node.file = new File(node.getParent().file, node.file.getName());
        }

        modifyChildren(ch -> ch.sort(comp));
    }

    @Override
    protected boolean canAddChild(FileArtifact toAdd) {

        if (!isDirectory()) {
            String msg = String.format(
                    "FileArtifact '%s' does not represent a directory. Can not add '%s' as a child.", this, toAdd);
            throw new IllegalStateException(msg);
        }

        return true;
    }

    @Override
    public FileArtifact copy() {
        return new FileArtifact(this);
    }

    @Override
    public FileArtifact createEmptyArtifact(Revision revision) {
        return new FileArtifact(revision, FileType.FILE);
    }

    @Override
    public String prettyPrint() {
        return getContent();
    }

    @Override
    public boolean exists() {
        return getFile().exists();
    }

    /**
     * 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()) {
            getChildren().stream().filter(FileArtifact::isDirectory).forEach(FileArtifact::filterNonJavaFiles);

            LOG.fine(() -> "Filtering out the children not representing java source code files from " + this);
            modifyChildren(
                    cs -> cs.removeIf(c -> c.isFile() && !c.isJavaFile() || c.isDirectory() && !c.hasChildren()));
        }
    }

    /**
     * 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() {
        String mimeType = null;
        File file = getFile();

        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 = getFile().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) {
            FileArtifact child = new FileArtifact(getRevision(), number, f, true);

            child.setParent(this);
            artifacts.add(child);
        }

        return artifacts;
    }

    /**
     * Returns the encapsulated file. The original file will be returned for non-virtual
     * {@link FileArtifact FileArtifacts}. If the {@link FileArtifact} is virtual, the returned {@link File} may not
     * exist.
     *
     * @return the encapsulated {@link File}
     */
    public File getFile() {
        return original != null ? original : file;
    }

    /**
     * Returns all {@link FileArtifact FileArtifacts} representing Java sourcecode files that are part of the
     * {@link FileArtifact} tree rooted in this {@link Artifact}.
     *
     * @return the Java sourcecode files as determined by {@link #isJavaFile()}
     */
    private List<FileArtifact> getJavaFiles() {
        return getJavaFiles(new ArtifactList<>());
    }

    /**
     * Returns all {@link FileArtifact FileArtifacts} representing Java sourcecode files that are part of the
     * {@link FileArtifact} tree rooted in this {@link Artifact}.
     *
     * @param list the {@link List} to append the Java sourcecode files to
     * @return the given {@code list} containing the Java sourcecode files as determined by {@link #isJavaFile()}
     */
    private List<FileArtifact> getJavaFiles(List<FileArtifact> list) {

        if (isJavaFile()) {
            list.add(this);
        } else if (isDirectory()) {
            getChildren().forEach(c -> c.getJavaFiles(list));
        }

        return list;
    }

    @Override
    public final String getId() {
        return getRevision() + ":" + getFile().getPath();
    }

    @Override
    protected String hashId() {
        if (isFile()) {
            return DigestUtils.sha256Hex(file.getName() + getContent());
        } else {
            return DigestUtils.sha256Hex(file.getName());
        }
    }

    @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(() -> getFile().getName());
    }

    /**
     * Returns true if artifact is a directory.
     *
     * @return true if artifact is a directory
     */
    public boolean isDirectory() {
        return type == FileType.DIR;
    }

    /**
     * Returns true if the artifact is empty.
     *
     * @return true if the artifact is empty
     */
    @Override
    public boolean isEmpty() {
        if (isDirectory()) {
            return !hasChildren();
        } else {
            return "".equals(getContent());
        }
    }

    /**
     * Returns true if artifact is a normal file.
     *
     * @return true if artifact is a normal file
     */
    public boolean isFile() {
        return type == FileType.FILE;
    }

    @Override
    public boolean isOrdered() {
        return false;
    }

    @Override
    public 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 boolean categoryMatches(FileArtifact other) {
        return isDirectory() && other.isDirectory() || isFile() && other.isFile();
    }

    @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 {
                try {
                    strategy.merge(operation, context);
                } catch (Throwable e) {

                    if (context.hasStatistics()) {
                        context.getStatistics().getScenarioStatistics(scenario).setStatus(FAILED);
                    }

                    throw e;
                }
            } 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).get());
                        merge(operation, context);
                    } else {
                        LOG.severe(() -> "Skipping " + scenario);
                    }
                }
            }
        }
    }

    @Override
    public final String toString() {
        return getFile().getName();
    }

    @Override
    public FileArtifact createConflictArtifact(FileArtifact left, FileArtifact right) {
        // This is not a conflict introduced by concurrent modification of content,
        // but by deleting and changing a file (insertion-deletion conflict on file/directory level)

        FileArtifact deleted = left != null ? left : right;
        assert deleted != null;

        if (deleted.isFile()) {
            FileArtifact conflict = new FileArtifact(deleted);

            StringBuilder content = new StringBuilder();
            content.append(Content.Conflict.CONFLICT_START);
            if (deleted == left) {
                content.append(" ").append(deleted.getRevision());
            }
            if (deleted == left) {
                content.append(System.lineSeparator()).append(deleted.content);
            }
            content.append(System.lineSeparator()).append(Content.Conflict.CONFLICT_DELIM);
            if (deleted == right) {
                content.append(System.lineSeparator()).append(deleted.content);
            }
            content.append(System.lineSeparator()).append(Content.Conflict.CONFLICT_END);
            if (deleted == right) {
                content.append(" ").append(deleted.getRevision());
            }
            content.append(System.lineSeparator());

            conflict.setContent(content.toString());

            return conflict;
        } else if (deleted.isDirectory()) {
            FileArtifact conflict = new FileArtifact(deleted);

            for (FileArtifact child : deleted.getChildren()) {
                if (deleted == left) {
                    conflict.addChild(createConflictArtifact(child, null));
                } else if (deleted == right) {
                    conflict.addChild(createConflictArtifact(null, child));
                } else {
                    throw new RuntimeException("Both sides of conflict are null");
                }
            }

            return conflict;
        } else {
            throw new RuntimeException("FileArtifact " + deleted + " is neither file nor directory.");
        }
    }

    @Override
    public FileArtifact createChoiceArtifact(String condition, FileArtifact artifact) {
        throw new NotYetImplementedException();
    }

    /**
     * Outputs the contents represented by this {@link FileArtifact} and its children using the given
     * {@link PrintStream}.
     *
     * @param to
     *         the {@link PrintStream} to write to
     */
    public void outputContent(PrintStream to) {

        if (isDirectory()) {
            getChildren().forEach(c -> {
                c.outputContent(to);
                to.println();
            });
        } else {
            to.print(getContent());
        }
    }

    /**
     * Recursively (over)writes the contents of this {@link FileArtifact} and its children to the files they represent.
     *
     * @throws IOException
     *         if there is an exception accessing the filesystem
     */
    public void writeContent() throws IOException {

        if (isFile()) {

            if (content != null) {
                writeToFile();
            } else if (original != null) {
                copyFile();
            } else {
                touchFile();
            }
        } else if (isDirectory()) {

            for (FileArtifact child : getChildren()) {
                child.writeContent();
            }
        }
    }

    /**
     * Writes the {@link #content} of this {@link FileArtifact} to its {@link #file}.
     *
     * @throws IOException
     *         see {@link FileUtils#openOutputStream(File)}
     */
    private void writeToFile() throws IOException {
        try (OutputStreamWriter out = new OutputStreamWriter(FileUtils.openOutputStream(file), UTF_8)) {
            out.write(content);
        }
    }

    /**
     * Copies the {@link #original} to the {@link #file} of this {@link FileArtifact}.
     *
     * @throws IOException
     *         see {@link FileUtils#copyFile(File, File)}
     */
    private void copyFile() throws IOException {
        if (!Files.isSameFile(original.toPath(), file.toPath())) {
            FileUtils.copyFile(original, file);
        }
    }

    /**
     * Creates an empty version of the {@link #file} on disk.
     *
     * @throws IOException
     *         see {@link FileUtils#touch(File)}
     */
    private void touchFile() throws IOException {
        FileUtils.touch(file);
    }

    /**
     * Returns the content of the {@link File} this {@link FileArtifact} represents. Will return an empty {@link String}
     * if there is an exception reading the content of non-virtual {@link FileArtifact FileArtifacts} or if the
     * {@link FileArtifact} is virtual and the content was not set to something other than an empty {@link String}.
     * Also returns an empty {@link String} for directories.
     *
     * @return the content this {@link FileArtifact} represents
     */
    public String getContent() {

        if (isDirectory()) {
            LOG.warning("Returning an empty string as the contents of the directory " + file);
            return "";
        }

        if (content == null) {
            String content;

            if (original == null) {
                content = "";
            } else {
                try {
                    content = FileUtils.readFileToString(original, UTF_8);
                } catch (IOException e) {
                    LOG.log(Level.WARNING, e, () -> "Could not read the contents of " + this);
                    return "";
                }
            }

            this.content = content;
        }

        return content;
    }

    /**
     * Sets the content this {@link FileArtifact} represents to the new value. If this {@link FileArtifact} represents
     * a directory, the call is ignored.
     *
     * @param content
     *         the new content
     */
    public void setContent(String content) {

        if (isFile()) {
            this.content = content;
        } else {
            LOG.warning("Ignoring a call to setContent(String) on a FileArtifact representing a directory.");
        }
    }
}