package de.duehl.basics.io;

/*
 * Copyright 2025 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.filechooser.FileSystemView;

import de.duehl.basics.caller.CallerDeterminer;
import de.duehl.basics.collections.CollectionsHelper;
import de.duehl.basics.collections.Stack;
import de.duehl.basics.datetime.DateAndTime;
import de.duehl.basics.datetime.data.DateAndTimeString;
import de.duehl.basics.datetime.date.DateHelper;
import de.duehl.basics.datetime.date.ImmutualDate;
import de.duehl.basics.datetime.time.ImmutualTime;
import de.duehl.basics.datetime.time.TimeHelper;
import de.duehl.basics.io.data.FileWithModificationTime;
import de.duehl.basics.io.exceptions.DirectoryNotCreatedRuntimeException;
import de.duehl.basics.io.exceptions.FileNotFoundRuntimeException;
import de.duehl.basics.io.exceptions.IORuntimeException;
import de.duehl.basics.io.textfile.StringsFromNormalTextFileReader;
import de.duehl.basics.io.textfile.StringsFromTextFileInJarReader;
import de.duehl.basics.io.textfile.StringsFromTextFileReader;
import de.duehl.basics.io.textfile.data.DataWithTitlesAndValues;
import de.duehl.basics.io.textfile.dictionary.Dictionary;
import de.duehl.basics.io.textfile.dictionary.io.reader.DictionaryFromNormalTextFileReader;
import de.duehl.basics.io.textfile.dictionary.io.reader.DictionaryFromTextFileInJarReader;
import de.duehl.basics.io.textfile.dictionary.io.reader.DictionaryFromTextFileReader;
import de.duehl.basics.system.SystemTools;
import de.duehl.basics.text.Lines;
import de.duehl.basics.text.NumberString;
import de.duehl.basics.text.Text;

/**
 * Diese Klasse enthält Hilfsmethoden rund um das Dateisystem.
 *
 * @version 1.01     2025-11-13
 * @author Christian Dühl
 */

public class FileHelper {

    public static final String NO_EXTENSION_FOUND = "OHNE_EXTENSION";

    private static final List<String> FORBIDDEN_FILENAME_CHARACTERS =
            CollectionsHelper.buildListFrom("<", ">", ":", "\"", "/", "\\", "|", "?", "*");

    /** Eine Instanz soll hiervon nicht angelegt werden! */
    private FileHelper() {}

    /**
     * Löscht ein Verzeichnis mit Inhalt.
     *
     * @param path
     *            Verzeichnis
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht löschen lässt.
     */
    public static void deleteTree(File path) {
        deleteTree(path, true);
    }

    private static void deleteTree(File path, boolean throwExceptionInCaseOfNotDeletable) {
        if (!path.exists()) {
            return;
        }
        for (File file : path.listFiles()) {
            if (file.isDirectory()) {
                deleteTree(file);
            }
            else if (!file.delete()) {
                if (throwExceptionInCaseOfNotDeletable) {
                    throw new IORuntimeException(file + " could not be deleted!");
                }
            }
        }

        if (!path.delete()) {
            if (throwExceptionInCaseOfNotDeletable) {
                throw new IORuntimeException(path + " could not be deleted!");
            }
        }
    } // TODO umstellen auf nio2

    /**
     * Löscht ein Verzeichnis mit Inhalt.
     *
     * @param path
     *            Verzeichnis
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht löschen lässt.
     */
    public static void deleteTree(String path) {
        deleteTree(new File(path));
    }

    /**
     * Löscht ein Verzeichnis mit Inhalt. Es ist nicht sicher, ob das Verzeichnis danach auch
     * wirklich gelöscht ist, aber er versucht jede Datei und jedes Verzeichnis zu löschen.
     *
     * @param path
     *            Verzeichnis
     */
    public static void deleteTreeIgnoreErrors(File path) {
        deleteTree(path, false);
    }

    /**
     * Löscht ein Verzeichnis mit Inhalt. Es ist nicht sicher, ob das Verzeichnis danach auch
     * wirklich gelöscht ist, aber er versucht jede Datei und jedes Verzeichnis zu löschen.
     *
     * @param path
     *            Verzeichnis
     */
    public static void deleteTreeIgnoreErrors(String path) {
        deleteTreeIgnoreErrors(new File(path));
    }

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis und seinen
     * Unterverzeichnissen.
     *
     * Hierbei spielt die Groß-/Kleinschreibung bei den Extensionen wie auch den Dateinamen keine
     * Rolle.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extensions
     *            Liste aller Endungen der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFiles(String dir, String... extensions) {
        if (0 == extensions.length) {
            throw new IllegalArgumentException("Mindestens eine Endung muss angegebene werden.");
        }

        List<String> lowerExtensions = new ArrayList<>();
        for (String extension : extensions) {
            lowerExtensions.add(extension.toLowerCase());
        }

        List<String> files = new ArrayList<>();
        for (File file : new File(dir).listFiles()) {
            String path = file.getPath();
            if (file.isDirectory()) {
                files.addAll(findFiles(path, extensions));
            }
            else {
                String lowerPath = path.toLowerCase();
                for (String lowerExtension : lowerExtensions) {
                    if (lowerPath.endsWith(lowerExtension)) {
                        files.add(path);
                    }
                }
            }
        }

        return files;
    }

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis und seinen
     * Unterverzeichnissen.
     *
     * Hierbei spielt die Groß-/Kleinschreibung bei den Extensionen wie auch den Dateinamen keine
     * Rolle.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extensions
     *            Liste aller Endungen der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesNio2Old(String dir, String... extensions) {
        if (0 == extensions.length) {
            throw new IllegalArgumentException("Mindestens eine Endung muss angegebene werden.");
        }

        List<String> lowerExtensions = new ArrayList<>();
        for (String extension : extensions) {
            lowerExtensions.add(extension.toLowerCase());
        }

        List<String> files = new ArrayList<>();
        for (String file : findAllFilesNio2(dir)) {
            String lowerFile = file.toLowerCase();
            for (String lowerExtension : lowerExtensions) {
                if (lowerFile.endsWith(lowerExtension)) {
                    files.add(file);
                }
            }
        }

        return files;
    }

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis und seinen
     * Unterverzeichnissen.
     *
     * Hierbei spielt die Groß-/Kleinschreibung bei den Extensionen wie auch den Dateinamen keine
     * Rolle.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extensions
     *            Liste aller Endungen der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesNio2(String dir, String... extensions) {
        if (0 == extensions.length) {
            throw new IllegalArgumentException("Mindestens eine Endung muss angegebene werden.");
        }

        List<String> lowerExtensions = new ArrayList<>();
        for (String extension : extensions) {
            lowerExtensions.add(extension.toLowerCase());
        }

        List<String> files = new ArrayList<>();
        Path dirPath = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath)) {
            Iterator<Path> iterator = dirStream.iterator();
            while (iterator.hasNext()) {
                Path file = iterator.next();
                if (Files.isRegularFile(file)) {
                    String fileAsString = file.toString();
                    String lowerFile = fileAsString.toLowerCase();
                    for (String lowerExtension : lowerExtensions) {
                        if (lowerFile.endsWith(lowerExtension)) {
                            files.add(fileAsString);
                            break;
                        }
                    }
                }
                else if (Files.isDirectory(file)) {
                    files.addAll(findFilesNio2(file.toString(), extensions));
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Dateien mit den Endungen "
                    + extensions + " im Verzeichnis " + dir, exception);
        }
        return files;
    }

    /**
     * Findet alle Dateien im angegebene Verzeichnis und seinen Unterverzeichnissen.
     *
     * @param dir
     *            Verzeichnis das durchsucht wird.
     * @return Liste der gefundenen Dateien.
     */
    public static List<String> findAllFilesNio2(String dir) {
        List<String> files = new ArrayList<>();
        Path dirPath = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath)) {
            Iterator<Path> iterator = dirStream.iterator();
            while (iterator.hasNext()) {
                Path file = iterator.next();
                if (Files.isRegularFile(file)) {
                    files.add(file.toString());
                }
                else if (Files.isDirectory(file)) {
                    files.addAll(findAllFilesNio2(file.toString()));
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Dateien im Verzeichnis "
                    + dir, exception);
        }
        return files;
    }

    /**
     * Findet alle Verzeichnisse im angegebene Verzeichnis und seinen Unterverzeichnissen.
     *
     * @param dir
     *            Verzeichnis das durchsucht wird.
     * @return Liste der gefundenen Dateien.
     */
    public static List<String> findAllDirectoriesNio2(String dir) {
        List<String> directories = new ArrayList<>();
        Path dirPath = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath)) {
            Iterator<Path> iterator = dirStream.iterator();
            while (iterator.hasNext()) {
                Path file = iterator.next();
                if (Files.isDirectory(file)) {
                    directories.add(file.toString());
                    directories.addAll(findAllDirectoriesNio2(file.toString()));
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Unterverzeichnissen im "
                    + "Verzeichnis " + dir, exception);
        }
        return directories;
    }

    /**
     * Sucht alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findAllFilesInMainDirectory(String dir) {
        List<String> files = new ArrayList<>();
        File startAsFile = new File(dir);
        for (File file : startAsFile.listFiles()) {
            if (file.isFile()) {
                files.add(file.getPath());
            }
        }
        return files;
    }

    /**
     * Sucht alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findAllFilesInMainDirectoryNio2(String dir) {
        List<String> files = new ArrayList<>();
        Path pathDir = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pathDir)) {
            Iterator<Path> imageIter = dirStream.iterator();
            while (imageIter.hasNext()) {
                Path file = imageIter.next();
                if (Files.isRegularFile(file)) {
                    files.add(file.toString());
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Dateien im Verzeichnis "
                    + dir, exception);
        }
        return files;
    }

    /**
     * Sucht alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien ohne Pfad.
     */
    public static List<String> findAllFilesInMainDirectoryWithoutPath(String dir) {
        List<String> list = findAllFilesInMainDirectory(dir);
        return removePath(list);
    }

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extension
     *            Endung der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectory(String dir, String extension) {
        List<String> files = new ArrayList<>();
        File startAsFile = new File(dir);
        String lowerExtension = extension.toLowerCase();
        for (File file : startAsFile.listFiles()) {
            if (file.getPath().toLowerCase().endsWith(lowerExtension)) {
                files.add(file.getPath());
            }
        }
        return files;
    }

    /**
     * Sucht alle Dateien mit bestimmten Endungen in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Das Verzeichnis, in dem die Suche durchgeführt wird.
     * @param extensions
     *            Die Endungen der gesuchten Dateien (etwa ".txt" oder ".java"). Diese kann mit
     *            oder ohne führenden Punkt angegebene werden.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectoryNio2WithExtensions(String dir,
            String ... extensions) {
        List<String> extensionList = CollectionsHelper.stringArrayToList(extensions);
        return findFilesInMainDirectoryNio2WithExtensions(dir, extensionList);
    }

    /**
     * Sucht alle Dateien mit bestimmten Endungen in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Das Verzeichnis, in dem die Suche durchgeführt wird.
     * @param extensions
     *            Die Endungen der gesuchten Dateien (etwa ".txt" oder ".java"). Diese kann mit
     *            oder ohne führenden Punkt angegebene werden.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectoryNio2WithExtensions(String dir,
            List<String> extensions) {
        List<String> files = new ArrayList<>();
        for (String ext : extensions) {
            if (ext.startsWith(".")) {
                ext = ext.substring(1);
            }
            Path pathDir = Paths.get(dir);
            addFilesInMainDirectoryNio2ToList(dir, pathDir, ext, files, extensions);
        }
        return files;
    }

    private static void addFilesInMainDirectoryNio2ToList(String dir, Path pathDir, String ext,
            List<String> files, List<String> extensions) {
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pathDir, "*." + ext)) {
            Iterator<Path> imageIter = dirStream.iterator();
            while (imageIter.hasNext()) {
                Path file = imageIter.next();
                if (Files.isRegularFile(file)) {
                    files.add(file.toString());
                }
            }
        }
        catch (IOException exception) {
            String message = "Fehler bei der Suche nach Dateien mit "
                    + (
                            extensions.size() == 1
                                    ? "der Endung '" + extensions.get(0) + "'"
                                    : "den Endungen '" + Text.join("', '", extensions)   + "'"
                      )
                    + " im Verzeichnis " + dir;
            throw new IllegalStateException(message, exception);
        }
    }

    /**
     * Sucht alle Dateien mit einem bestimmten Anfang in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param start
     *            Ende der gesuchten Dateien (etwa "_dump.zip").
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectoryNio2StartsWith(String dir, String start) {
        List<String> files = new ArrayList<>();
        Path pathDir = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pathDir, start + "*.*")) {
            Iterator<Path> imageIter = dirStream.iterator();
            while (imageIter.hasNext()) {
                Path file = imageIter.next();
                if (Files.isRegularFile(file)) {
                    files.add(file.toString());
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Dateien mit dem Anfang "
                    + start + " im Verzeichnis " + pathDir, exception);
        }
        return files;
    }

    /**
     * Sucht alle Dateien mit einem bestimmten Ende in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * Achtung, wenn es nur um die Extension geht, sollte besser die Methode
     * findFilesInMainDirectoryNio2() verwendet werden!
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param end
     *            Ende der gesuchten Dateien (etwa "_dump\\.zip").
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectoryNio2EndsWith(String dir, String end) {
        List<String> files = new ArrayList<>();
        Path pathDir = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pathDir, "*" + end)) {
            Iterator<Path> imageIter = dirStream.iterator();
            while (imageIter.hasNext()) {
                Path file = imageIter.next();
                if (Files.isRegularFile(file)) {
                    files.add(file.toString());
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException("Fehler bei der Suche nach Dateien mit dem Ende " + end
                    + " im Verzeichnis " + dir, exception);
        }
        return files;
    }

    /**
     * Sucht alle Dateien mit einem bestimmten Ende in einem Verzeichnis und in seinen
     * Unterverzeichnissen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param end
     *            Ende der gesuchten Dateien (etwa "_dump\\.zip").
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesNio2EndsWith(String dir, String end) {
        List<String> subdirs = findAllDirectoriesNio2(dir);
        subdirs.add(dir);

        List<String> files = new ArrayList<>();
        for (String subdir : subdirs) {
            files.addAll(findFilesInMainDirectoryNio2EndsWith(subdir, end));
        }

        return files;
    }

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param front
     *            Der Name der Dateien (ohne Pfad) muss hiermit beginnen.
     * @param extension
     *            Endung der gesuchten Dateien (etwa ".txt" oder ".java"). Diese kann mit oder ohne
     *            führenden Punkt angegebene werden.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findFilesInMainDirectoryNio2(String dir, String front,
            String extension) {
        List<String> files = findFilesInMainDirectoryNio2WithExtensions(dir, extension);
        List<String> rightFiles = new ArrayList<>();

        for (String filename : files) {
            String barename = getBareName(filename);
            if (barename.startsWith(front)) {
                rightFiles.add(filename);
            }
        }

        return rightFiles;
    }// TODO TESTEN

    /**
     * Sucht alle Dateien mit einer bestimmten Endung in einem Verzeichnis ohne seine
     * Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extension
     *            Endung der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien ohne Pfad.
     */
    public static List<String> findFilesInMainDirectoryWithoutPath(String dir, String extension) {
        List<String> list = findFilesInMainDirectory(dir, extension);
        return removePath(list);
    }// TODO TESTEN

    /**
     * Sucht alle Verzeichnisse in einem Verzeichnis ohne seine Unterverzeichnisse zu
     * berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Verzeichnissen.
     */
    public static List<String> findAllSubdirectoriesInMainDirectoryNio2(String dir) {
        List<String> files = new ArrayList<>();
        Path pathDir = Paths.get(dir);
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(pathDir)) {
            Iterator<Path> imageIter = dirStream.iterator();
            while (imageIter.hasNext()) {
                Path file = imageIter.next();
                if (Files.isDirectory(file, LinkOption.NOFOLLOW_LINKS)) {
                    files.add(file.toString());
                }
            }
        }
        catch (IOException exception) {
            throw new IllegalStateException(
                    "Fehler bei der Suche nach Unterverzeichnissen im Verzeichnis " + dir,
                    exception);
        }
        return files;
    }// TODO TESTEN

    /**
     * Sucht alle direkten Unterverzeichnisse in einem Verzeichnis.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findSubdirectoriesInMainDirectory(String dir) {
        List<String> files = new ArrayList<>();
        File startAsFile = new File(dir);
        for (File file : startAsFile.listFiles()) {
            if (file.isDirectory()) {
                files.add(file.getPath());
            }
        }
        return files;
    }// TODO TESTEN

    /**
     * Sucht alle direkten Unterverzeichnisse in einem Verzeichnis.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien ohne Pfad.
     */
    public static List<String> findSubdirectoriesInMainDirectoryWithoutPath(String dir) {
        List<String> list = findSubdirectoriesInMainDirectory(dir);
        return removePath(list);
    }// TODO TESTEN

    /**
     * Sucht alle direkten Unterverzeichnisse in einem Verzeichnis.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param pattern
     *            Das Pattern muss exakt auf den Namen des Verzeichnisses (ohne Pfad) passen.
     * @return Liste mit den gefundenen Dateien.
     */
    public static List<String> findSubdirectoriesInMainDirectory(String dir, Pattern pattern) {
        List<String> files = new ArrayList<>();
        File dirAsFile = new File(dir);
        for (File file : dirAsFile.listFiles()) {
            if (file.isDirectory()) {
                String fileWithPath = file.getPath();
                String fileWithoutPath = getBareName(fileWithPath);
                Matcher matcher = pattern.matcher(fileWithoutPath);
                if (matcher.matches()) {
                    files.add(fileWithPath);
                }
            }
        }
        return files;
    }// TODO TESTEN

    /**
     * Sucht alle direkten Unterverzeichnisse in einem Verzeichnis.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param pattern
     *            Das Pattern muss exakt auf den Namen des Verzeichnisses (ohne Pfad) passen.
     * @return Liste mit den gefundenen Dateien ohne Pfad.
     */
    public static List<String> findSubdirectoriesInMainDirectoryWithoutPath(String dir,
            Pattern pattern) {
        List<String> list = findSubdirectoriesInMainDirectory(dir, pattern);
        return removePath(list);
    }// TODO TESTEN

    /**
     * Sucht alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @return Liste mit den gefundenen Dateien (mit Pfaden).
     */
    public static List<String> findAllNormalFilesInMainDirectory(String dir) {
        // TODO das ist eigentlich obsolet... -> findAllFilesInMainDirectoryNio2
        List<String> files = new ArrayList<>();
        File dirAsFile = new File(dir);
        for (File file : dirAsFile.listFiles()) {
            if (file.isFile()) {
                files.add(file.getPath());
            }
        }
        return files;
    }// TODO TESTEN

    /**
     * Ermittelt, ob es mindestens eine Datei mit einer bestimmten Endung in einem Verzeichnis und
     * seinen Unterverzeichnissen gibt.
     *
     * @param dir
     *            Verzeichnis, in dem die Suche beginnt.
     * @param extension
     *            Endung der gesuchten Dateien (etwa ".txt" oder ".java")
     * @return Liste mit den gefundenen Dateien.
     */
    public static boolean containsFiles(String dir, String extension) {
        File dirAsFile = new File(dir);
        for (File file : dirAsFile.listFiles()) {
            if (file.isDirectory()) {
                if (containsFiles(file.getPath(), extension)) {
                    return true;
                }
            }
            else {
                if (file.getPath().endsWith(extension)) {
                    // System.out.println("file " + file);
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Ermittelt die Position des ersten Slash oder Backslash in einem Dateinamen.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Letzte Position (beginnend bei 0) oder -1, wenn weder Slash noch Backslash in der
     *         Eingabe enthalten waren.
     */
    public static int firstSlash(String filename) {
        int firstSlash = filename.indexOf("/");
        int firstBackslash = filename.indexOf("\\");

        if (firstSlash == -1) {
            return firstBackslash;// TODO TESTEN
        }
        else if (firstBackslash == -1) {
            return firstSlash;
        }
        else if (firstSlash < firstBackslash) {
            return firstSlash;
        }
        else {
            return firstBackslash;
        }
    }

    /**
     * Ermittelt die Position des letzten Slash oder Backslash in einem Dateinamen.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Letzte Position (beginnend bei 0) oder -1, wenn weder Slash noch Backslash in der
     *         Eingabe enthalten waren.
     */
    public static int lastSlash(String filename) {
        int lastSlash = filename.lastIndexOf("/");
        int lastBackslash = filename.lastIndexOf("\\");

        if (lastSlash > lastBackslash) {
            return lastSlash;
        }
        else {
            return lastBackslash;
        }
    }

    /**
     * Entfernt einen Slash oder Backslash am Ende des übergebenen Namens, falls dieser auf einen
     * solchen endet. Anderenfalls wird der originale Name zurückgegeben.
     *
     * @param filename
     *            Zu bearbeitender Dateiname.
     * @return Dateiname ohne Verzeichnistrenner am Ende.
     */
    public static String removeTrailingSlash(String filename) {
        int lastSlash = lastSlash(filename);
        if (lastSlash == filename.length() - 1) {
            return filename.substring(0, filename.length() - 1);
        }
        else {
            return filename;
        }
    }

    /**
     * Normalisiert alle Backslashes zu Slashes.
     *
     * @param path
     *            Zu normalisierender Pfad.
     * @return Normalisierter Pfad.
     */
    public static String normalizeToSlahes(String path) {
        return path.replace("\\", "/");
    }

    /**
     * Normalisiert alle Slashes zu Backslashes.
     *
     * @param path
     *            Zu normalisierender Pfad.
     * @return Normalisierter Pfad.
     */
    public static String normalizeToBackSlahes(String path) {
        return path.replace("/", "\\");
    }

    /** Teilt einen Pfad an den Slashes oder Backslashes auf. */
    public static List<String> splitPath(String path) {
        return Text.splitBy(path, "[/\\\\]");
    }

    /**
     * Ermittelt den Teil des übergebenen Dateinamens vor und inklusive des letzten Slashs oder
     * Backslashs.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Teil des Dateinamens vor dem letzten Slash oder Backslash exklusive dieses Zeichens.
     */
    public static String getDirName(String filename) {
        int index = lastSlash(filename);
        if (index == filename.length() - 1) {
            return getDirName(filename.substring(0, filename.length() - 1));
        }
        else {
            return filename.substring(0, index + 1);
        }
    }

    /**
     * Ermittelt den Teil des übergebenen Dateinamens vor und inklusive des letzten Slashs oder
     * Backslashs.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Teil des Dateinamens vor dem letzten Slash oder Backslash exklusive dieses Zeichens.
     */
    public static String getDirNameWithoutLastSlash(String filename) {
        return removeTrailingSlashOrBackslash(getDirName(removeTrailingSlashOrBackslash(filename)));
        /*
        String dirNameWithSlash = getDirName(filename);
        return dirNameWithSlash.substring(0, dirNameWithSlash.length() - 1);
        */
    }

    /** Entfernt Slash oder Backslash am Ende, falls vorhanden. */
    public static String removeTrailingSlashOrBackslash(String input) {
        String corrected = input;
        for (String toRemove : CollectionsHelper.buildListFrom("/", "\\")) {
            if (corrected.endsWith(toRemove)) {
                corrected = Text.removeTextAtEndIfEndsWith(corrected, toRemove);
                break; // nur das hinten als erstes vorkommende Zeichen entfernen!
            }
        }

        return corrected;
    }

    /**
     * Ermittelt den Teil des übergebenen Dateinamens nach dem letzten Slash oder Backslash.
     *
     * @param file
     *            Zu untersuchende Datei.
     * @return Teil des Dateinamens nach dem letzten Slash oder Backslash. Falls der String mit
     *         einem solchen Zeichen endet, wird ein leerer String zurückgegeben.
     */
    public static String getBareName(File file) {
        return getBareName(file.getPath());
    }// TODO TESTEN

    /**
     * Ermittelt den Teil des übergebenen Dateinamens nach dem letzten Slash oder Backslash.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Teil des Dateinamens nach dem letzten Slash oder Backslash. Falls der String mit
     *         einem solchen Zeichen endet, wird ein leerer String zurückgegeben.
     */
    public static String getBareName(String filename) {
        int index = lastSlash(filename);

        if (filename.length() > index + 1) {
            return filename.substring(index + 1);
        }
        else {
            return "";
        }

        // alternativ: (new File(filename)).getName();
    }

    /**
     * Ermittelt den Teil des übergebenen Dateinamens nach dem letzten Slash oder Backslash.
     * Ein Backslash am Ende wird allerdings entfernt.
     *
     * @param filename
     *            Zu untersuchender Dateiname.
     * @return Teil des Dateinamens nach dem letzten Slash oder Backslash. Falls der String mit
     *         einem solchen Zeichen endet, wird der Teil davor zurückgegeben.
     */
    public static String getBareNameWithoutLastSlash(String filename) {
        return getBareName(removeTrailingSlashOrBackslash(filename));
        /*
        String corrrected = filename;
        corrrected = Text.removeTextAtEndIfEndsWith(corrrected, "/");
        corrrected = Text.removeTextAtEndIfEndsWith(corrrected, "\\");
        return getBareName(corrrected);
        */
    }// TODO TESTEN

    /**
     * Es wird der Pfad für ~/localDirectory zurückgegeben. Falls es nicht existiert, wird es
     * angelegt.
     *
     * @param subDirWithoutPath
     *            Name des lokalen Verzeichnisses (etwa ".Jarify").
     * @return Pfad für ~/localDirectory.
     * @throws FileNotFoundRuntimeException
     *             Falls das Home-Verzeichnis nicht existiert.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls ~/localDirectory nicht existiert und nicht angelegt werden kann.
     */
    public static String getAndPerhapsCreateDirectoryAtHome(String subDirWithoutPath) {
        String home = SystemTools.getHomeDirectory();
        return createSubdirectoryWithMissingSubdirectoriesInExistingDirectoryIfNecessary(home,
                subDirWithoutPath);
    }// TODO TESTEN

    /**
     * Es wird ein Unterverzeichnis in einem bestehenden Verzeichnis angelegt,
     * falls es noch nicht besteht.                                              <br/><br/>
     *
     * Im Beispiel mit dir = c:/temp und subDirWithoutPath = blubb wird das
     * Verzeichnis c:/temp/blubb angelegt, wenn es c:/temp gibt und
     * c:/temp/blubb noch nicht gibt.
     *
     * @param dir
     *            Name des bestehenden, übergeordneten Verzeichnisses (etwa
     *            "c:/temp").
     * @param subDirWithoutPath
     *            Name des Unterverzeichnisses (ohne Pfad), das darunter
     *            angelegt werden soll, etwa "blubb".
     * @return Verzeichnis, das angelegt wurde, falls es nicht schon existierte.
     * @throws FileNotFoundRuntimeException
     *             Falls das Home-Verzeichnis nicht existiert.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls ~/localDirectory nicht existiert und nicht angelegt
     *             werden kann.
     */
    public static String createSubdirectoryInExistingDirectoryIfNecessary(String dir,
            String subDirWithoutPath) {
        if (!isDirectory(dir)) {
            throw new FileNotFoundRuntimeException("Das Verzeichnis '" + dir + "' existiert nicht!");
        }

        String dirAtDir = concatPathes(dir, subDirWithoutPath);
        createDirectoryIfNotExists(dirAtDir);

        return dirAtDir;
    }// TODO TESTEN

    /**
     * Es wird ein Unterverzeichnis in einem bestehenden Verzeichnis angelegt,
     * falls es noch nicht besteht.                                              <br/><br/>
     *
     * Anders als createSubdirectoryInExistingDirectoryIfNecessary() werden
     * auch fehlende Zwischen-Verzeichnisse angelegt.                            <br/><br/>
     *
     * Im Beispiel mit dir = c:/temp und subDirWithoutPath = blubb wird das
     * Verzeichnis c:/temp/blubb angelegt, wenn es c:/temp gibt und
     * c:/temp/blubb noch nicht gibt.
     *
     * @param dir
     *            Name des bestehenden, übergeordneten Verzeichnisses (etwa
     *            "c:/temp").
     * @param subDirWithoutPath
     *            Name des Unterverzeichnisses (ohne Pfad), das darunter
     *            angelegt werden soll, etwa "blubb".
     * @return Verzeichnis, das angelegt wurde, falls es nicht schon existierte.
     * @throws FileNotFoundRuntimeException
     *             Falls das Home-Verzeichnis nicht existiert.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls ~/localDirectory nicht existiert und nicht angelegt
     *             werden kann.
     */
    public static String createSubdirectoryWithMissingSubdirectoriesInExistingDirectoryIfNecessary(
            String dir, String subDirWithoutPath) {
        if (!isDirectory(dir)) {
            throw new FileNotFoundRuntimeException("Das Verzeichnis '" + dir + "' existiert nicht!");
        }

        String dirAtDir = concatPathes(dir, subDirWithoutPath);
        createDirectoryWithMissingSubdirectoriesIfNotExists(dirAtDir);

        return dirAtDir;
    }// TODO TESTEN

    /**
     * Legt ein Verzeichnis an.
     *
     * @param dirName
     *            Name des Verzeichnisses.
     * @return true genau dann, wenn das Verzeichnis angelegt wurde. false anderenfalls.
     */
    public static boolean createDirectory(String dirName) {
        File dir = new File(dirName);
        boolean success = dir.mkdir();
        return success;
    }

    /**
     * Legt ein Verzeichnis samt eventuell nicht vorhandener Unterverzeichnisse an, falls sie nicht
     * vorhanden sind.
     *
     * @param dirName
     *            Name des Verzeichnisses.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls das Verzeichnis nicht angelegt werden konnte, obwohl es nicht existierte.
     */
    public static void createDirectoryWithMissingSubdirectoriesIfNotExists(String dirName) {
        Stack<String> missingSubdirectories = new Stack<>();

        String path = Text.removeTrailingSlash(dirName);
        while (!exists(path)) {
            String subDir = getBareName(path);
            path = getDirNameWithoutLastSlash(path);
            missingSubdirectories.push(subDir);
        }

        while (!missingSubdirectories.isEmpty()) {
            String subDir = missingSubdirectories.pop();
            path = concatPathes(path, subDir);
            createDirectory(path);
        }
    }

    /**
     * Legt ein Verzeichnis an, falls es nicht vorhanden ist.
     *
     * @param dirName
     *            Name des Verzeichnisses.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls das Verzeichnis nicht angelegt werden konnte, obwohl es nicht existierte.
     */
    public static void createDirectoryIfNotExists(String dirName) {
        if (!exists(dirName)) {
            boolean success = createDirectory(dirName);
            if (!success) {
                throw new DirectoryNotCreatedRuntimeException("Das Verzeichnis " + dirName
                        + " kann nicht angelegt werden.");
            }
        }
    }

    /**
     * Legt ein Verzeichnis an, das vorher nicht vorhanden sein darf.
     *
     * @param dirName
     *            Name des Verzeichnisses.
     * @throws DirectoryNotCreatedRuntimeException
     *             Falls das Verzeichnis nicht angelegt werden konnte, obwohl es nicht existierte.
     */
    public static void createNotExistingDirectory(String dirName) {
        if (exists(dirName)) {
            throw new RuntimeException("Das Verzeichnis '" + dirName
                    + "', das angelegt werden soll, existiert bereits!");
        }
        boolean success = createDirectory(dirName);
        if (!success) {
            throw new RuntimeException("Das Verzeichnis '" + dirName
                    + "', das angelegt werden soll, konnte nicht angelegt werden!");
        }
    }// TODO TESTEN

    /**
     * Erstellt alle nicht vorhandenen Verzeichnisse zum gegebenen Dateinamen. Hinterher könnte man
     * dann die Datei kopieren, da das Verzeichnis existiert.
     *
     * @param filename
     *            Dateiname, zu dem ggf. fehlende Verzeichnisse angelegt werden sollen.
     */
    public static void createMissingDirectoriesForFile(String filename) {
        String path = getDirNameWithoutLastSlash(filename);
        createDirectoryWithMissingSubdirectoriesIfNotExists(path);
    }

    /**
     * Kopiert eine einzelne Datei mit NIO-Methoden.
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Kopieren schief geht.
     */
    public static void copyFile(String source, String target) {
        Path sourcePath = Paths.get(source);
        Path targetPath = Paths.get(target);
        try {
            Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
        catch (IOException exception) {
            throw new IORuntimeException(exception);
        }
    }// TODO TESTEN

    /**
     * Kopiert eine einzelne Datei mit NIO-Methoden.
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Kopieren schief geht.
     */
    public static void copyFile(File source, File target) {
        copyFile(source.getAbsolutePath(), target.getAbsolutePath());
    }// TODO TESTEN

    /**
     * Kopiert eine einzelne Datei mit NIO-Methoden.
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Kopieren schief geht.
     */
    public static void copyFile(File source, String target) {
        copyFile(source.getAbsolutePath(), target);
    }// TODO TESTEN

    /**
     * Verschiebt eine einzelne Datei mit NIO-Methoden.
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Verschieben schief geht.
     */
    public static void moveFile(String source, String target) {
        Path sourcePath = Paths.get(source);
        Path targetPath = Paths.get(target);
        try {
            Files.move(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
        catch (IOException exception) {
            throw new IORuntimeException(exception);
        }
    }// TODO TESTEN

    /**
     * Verschiebt eine einzelne Datei mit NIO-Methoden.
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Verschieben schief geht.
     */
    public static void moveFile(File source, File target) {
        moveFile(source.getAbsolutePath(), target.getAbsolutePath());
    }// TODO TESTEN

    /**
     * Verschiebt eine einzelne Datei (mit Hilfe von copyFile() und deleteFile()).
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Kopieren schief geht.
     */
    public static void moveFileViaCopyAndDelete(String source, String target) {
        copyFile(source, target);
        deleteFile(source);
    }// TODO TESTEN

    /**
     * Verschiebt eine einzelne Datei (mit Hilfe von copyFile() und deleteFile()).
     *
     * @param source
     *            Quelldatei
     * @param target
     *            Zieldatei
     * @throws IORuntimeException
     *             Falls etwas beim Kopieren schief geht.
     */
    public static void moveFileViaCopyAndDelete(File source, File target) {
        moveFileViaCopyAndDelete(source.getAbsolutePath(), target.getAbsolutePath());
    }// TODO TESTEN

    /**
     * Kopiert ein Verzeichnis mit Inhalt in ein anderes.
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void copyTree(String source, String target) {
        copyTree(new File(source), new File(target));
    }// TODO TESTEN

    /**
     * Kopiert ein Verzeichnis mit Inhalt in ein anderes.
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void copyTree(File source, String target) {
        copyTree(source, new File(target));
    }// TODO TESTEN

    /**
     * Kopiert ein Verzeichnis mit Inhalt in ein anderes.
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void copyTree(File source, File target) {
        if (!source.exists()) {
            return;
        }
        createDirectoryIfNotExists(target.getPath());
        for (File file : source.listFiles()) {
            if (file.isDirectory()) {
                String dirname = getBareName(file);
                String targetDir = concatPathes(target, dirname);
                createNotExistingDirectory(targetDir);
                copyTree(file, targetDir);
            }
            else {
                String dirname = getBareName(file);
                String targetFilename = concatPathes(target, dirname);
                copyFile(file, targetFilename);
            }
        }
    }// TODO TESTEN

    /**
     * Verschiebt ein Verzeichnis mit Inhalt in ein anderes.
     *
     * Achtung, diese dürfen sich nicht überlappen!
     *
     * Will man innerhalb der gleichen Partition verschieben, ist moveFile() wesentlich schneller!
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void moveTree(String source, String target) {
        moveTree(new File(source), new File(target));
    }// TODO TESTEN

    /**
     * Verschiebt ein Verzeichnis mit Inhalt in ein anderes.
     *
     * Achtung, diese dürfen sich nicht überlappen!
     *
     * Will man innerhalb der gleichen Partition verschieben, ist moveFile() wesentlich schneller!
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void moveTree(File source, String target) {
        moveTree(source, new File(target));
    }// TODO TESTEN

    /**
     * Verschiebt ein Verzeichnis mit Inhalt in ein anderes.
     *
     * Achtung, diese dürfen sich nicht überlappen!
     *
     * Will man innerhalb der gleichen Partition verschieben, ist moveFile() wesentlich schneller!
     *
     * @param source
     *            Quell-Verzeichnis.
     * @param target
     *            Ziel-Verzeichnis.
     * @throws IORuntimeException
     *             Wenn sich eine Datei oder Verzeichnis nicht kopieren oder anlegen lässt.
     */
    public static void moveTree(File source, File target) {
        copyTree(source, target);
        deleteTree(source);
    }// TODO TESTEN

    /**
     * Prüft, ob die Datei mit dem gegebenen Dateinamen existiert.
     *
     * @param filename
     *            Name der Datei.
     * @return Wahrheitswert.
     */
    public static boolean exists(String filename) {
        File file = new File(filename);
        return file.exists();
    }

    /**
     * Prüft, ob die Datei mit dem gegebenen Dateinamen existiert und eine normale Date ist.
     *
     * @param filename
     *            Name der Datei.
     * @return Wahrheitswert.
     */
    public static boolean isFile(String filename) {
        File file = new File(filename);
        return file.isFile();
    }

    /**
     * Prüft, ob das Verzeichnis mit dem gegebenen Dateinamen existiert und ein Verzeichnis ist.
     *
     * @param dir
     *            Name des Verzeichnisses.
     * @return Wahrheitswert.
     */
    public static boolean isDirectory(String dir) {
        File file = new File(dir);
        return file.isDirectory();
    }

    /**
     * Prüft, ob das Verzeichnis mit dem gegebenen Dateinamen nicht existiert oder kein Verzeichnis
     * ist.
     *
     * @param dir
     *            Name des Verzeichnisses.
     * @return Wahrheitswert.
     */
    public static boolean isNotADirectory(String dir) {
        return !isDirectory(dir);
    }

    /**
     * Löscht die übergebenen Dateien.
     *
     * @param filenames
     *            Zu löschende Dateien.
     * @throws IORuntimeException
     *             Falls sich eine der Dateien nicht löschen lässt.
     */
    public static void deleteFiles(List<String> filenames) {
        for (String filename : filenames) {
            deleteFile(filename);
        }
    }// TODO TESTEN

    /**
     * Löscht die übergebenen Dateien.
     *
     * Bei den Dateien, die sich nicht löschen lassen erfolgt eine Ausgabe nach Stdout.
     *
     * @param filenames
     *            Zu löschende Dateien.
     * @throws IORuntimeException
     *             Falls sich eine der Dateien nicht löschen lässt.
     */
    public static void deleteFilesPrintErrors(List<String> filenames) {
        for (String filename : filenames) {
            deleteFilePrintError(filename);
        }
    }// TODO TESTEN

    /**
     * Löscht eine Datei.
     *
     * @param filename
     *            Zu löschende Datei.
     * @throws IORuntimeException
     *             Falls sich die Datei nicht löschen lässt.
     */
    public static void deleteFile(String filename) {
        try {
            Files.delete(Paths.get(filename));
        }
        catch (IOException e) {
            throw new IORuntimeException(filename + " konnte nicht gelöscht werden!", e);
        }
        /*
        File file = new File(filename);
        if (!file.delete()) {
            throw new IORuntimeException(file + " could not be deleted!");
        }
        */
    }// TODO TESTEN

    /**
     * Löscht eine Datei. Wenn das nicht klappt, erfolgt eine Ausgabe nach Stdout.
     *
     * @param filename
     *            Zu löschende Datei.
     * @throws IORuntimeException
     *             Falls sich die Datei nicht löschen lässt.
     */
    public static void deleteFilePrintError(String filename) {
        try {
            Files.delete(Paths.get(filename));
        }
        catch (IOException exception) {
            System.out.println("Die Datei\n"
                    + "\t" + filename + "\n"
                    + "lässt sich nicht löschen: " + exception.getMessage());
        }
    }// TODO TESTEN

    /**
     * Löscht eine Datei, falls sie existiert.
     *
     * @param filename
     *            Zu löschende Datei.
     * @throws IORuntimeException
     *             Falls sich die Datei nicht löschen lässt.
     */
    public static void deleteFileIfExistent(String filename) {
        if (exists(filename)) {
            deleteFile(filename);
        }
    }// TODO TESTEN

    /**
     * Löscht alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen.
     *
     * @param start
     *            Verzeichnis, in dem die direkt enthaltenen Dateien gelöscht werden.
     */
    public static void deleteAllFilesInMainDirectory(String start) {
        List<String> filenames = findAllFilesInMainDirectoryNio2(start);
        for (String filename : filenames) {
            deleteFile(filename);
        }
    }// TODO TESTEN

    /**
     * Löscht (fast) alle Dateien in einem Verzeichnis ohne seine Unterverzeichnisse zu
     * berücksichtigen.
     *
     * @param start
     *            Verzeichnis, in dem die direkt enthaltenen Dateien gelöscht werden.
     * @param bareFilenamesToKeep
     *            Die Namen der zu behaltenden Dateien ohne Pfad.
     */
    public static void deleteMostFilesInMainDirectory(String start,
            List<String> bareFilenamesToKeep) {
        List<String> filenames = findAllFilesInMainDirectoryNio2(start);
        for (String filename : filenames) {
            String bareFilename = getBareName(filename);
            if (!bareFilenamesToKeep.contains(bareFilename)) {
                deleteFile(filename);
            }
        }
    }// TODO TESTEN

    /**
     * Kopiert alle Dateien aus einem Verzeichnis ohne seine Unterverzeichnisse zu berücksichtigen,
     * in ein anderes.
     *
     * @param start
     *            Verzeichnis, dessen direkt enthaltenen Dateien kopiert werden.
     * @param target
     *            Verzeichnis, in das die Dateien kopiert werden.
     */
    public static void copyAllFilesInMainDirectory(String start, String target) {
        List<String> files = findAllFilesInMainDirectory(start);
        for (String file : files) {
            String bareFile = getBareName(file);
            String copiedFile = concatPathes(target, bareFile);
            copyFile(file, copiedFile);
        }
    }// TODO TESTEN

    /**
     * Legt eine Datei leer an. Wenn es sie schon gibt, wird sie leer überschrieben.
     *
     * @param filename
     *            Name der Datei.
     */
    public static void createEmptyFile(String filename) {
        FineFileWriter writer = new FineFileWriter(filename);
        writer.close();
    }// TODO TESTEN

    /**
     * Erzeugt eine Datei leer, falls sie nicht bereits existiert.
     *
     * @param filename
     *            Name der Datei.
     */
    public static void createEmptyFileIfNotExisting(String filename) {
        if (!exists(filename)) {
            createEmptyFile(filename);
        }
    }// TODO TESTEN

    /**
     * Legt eine Datei leer an. Wenn es sie schon gibt, wird sie leer überschrieben.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Zeichensatz der zu schreibenden Datei.
     */
    public static void createEmptyFile(String filename, Charset charset) {
        FineFileWriter writer = new FineFileWriter(filename, charset);
        writer.close();
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in einen String und gibt diese
     * zurück.
     *
     * @param filename
     *            Name der Datei.
     * @return Inhalt der Datei.
     */
    public static String readFileToString(String filename) {
        Reader reader = new FineFileReader(filename);
        String contents = reader.readFileToString();
        reader.close();
        return contents;
    }

    /**
     * Liest den Inhalt der Datei im angegebenen Encoding mit dem gegebenen Dateinamen in einen
     * String und gibt diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Encoding der Datei.
     * @return Inhalt der Datei.
     */
    public static String readFileToString(String filename, Charset charset) {
        Reader reader = new FineFileReader(filename, charset);
        String contents = reader.readFileToString();
        reader.close();
        return contents;
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in einen String und gibt diese
     * zurück.
     *
     * @param file
     *            Datei.
     * @return Inhalt der Datei.
     */
    public static String readFileToString(File file) {
        return readFileToString(file.getPath());
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste seiner Zeilen und gibt
     * diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @return Inhalt der Datei als Liste der Zeilen.
     */
    public static List<String> readFileToList(String filename) {
        return readFromReaderToList(new FineFileReader(filename));
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste seiner Zeilen und gibt
     * diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Encoding der Datei.
     * @return Inhalt der Datei als Liste der Zeilen.
     */
    public static List<String> readFileToList(String filename, Charset charset) {
        return readFromReaderToList(new FineFileReader(filename, charset));
    }

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste seiner Zeilen und gibt
     * diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @return Inhalt der Datei als Liste der Zeilen.
     */
    public static List<String> readFileToListWithoutTitleLine(String filename) {
        List<String> lines = FileHelper.readFileToList(filename);
        if (!lines.isEmpty()) {
            lines.remove(0); // Titelzeile entfernen
        }
        return lines;
    }// TODO TESTEN

    /**
     * Liest die Zeilen aus dem übergebenen Reader in eine Liste ein und schließt den Reader dann.
     *
     * @param reader
     *            Zu verwendender Reader.
     * @return Liste mit den Zeilen.
     */
    private static List<String> readFromReaderToList(Reader reader) {
        List<String> contents = reader.readFileToListOfStrings();
        reader.close();
        return contents;
    }

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste
     * seiner Zeilen, wobei leere Zeilen und solche, die mit '#' anfangen,
     * übersprungen werden.                                                     <br><br>
     *
     * Die Zeilen werden nicht um vorn oder hinten befindliche Leerzeichen
     * bereinigt.
     *
     * @param filename
     *            Name der Datei.
     * @return Inhalt der Datei ohne Leerzeilen und Kommentare als Liste der Zeilen.
     */
    public static List<String> readFileToListIgnoreBlankAndCommentLines(String filename) {
        List<String> list = new ArrayList<>();

        Reader reader = new FineFileReader(filename);
        String line;
        while (null != (line = reader.readNextLine())) {
            String testLine = line.trim();
            if (!testLine.isEmpty() && !testLine.startsWith("#")) {
                list.add(line);
            }
        }

        return list;
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste seiner Zeilen und gibt
     * diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @return Inhalt der Datei als Liste der Zeilen.
     */
    public static List<List<String>> readFileToListOfFieldLines(String filename) {
        return readFromReaderToListOfFieldLines(new FineFileReader(filename));
    }// TODO TESTEN

    /**
     * Liest den Inhalt der Datei mit dem gegebenen Dateinamen in eine Liste seiner Zeilen und gibt
     * diese zurück.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Encoding der Datei.
     * @return Inhalt der Datei als Liste der Zeilen.
     */
    public static List<List<String>> readFileToListOfFieldLines(String filename, Charset charset) {
        return readFromReaderToListOfFieldLines(new FineFileReader(filename, charset));
    }// TODO TESTEN

    /**
     * Liest die Zeilen aus dem übergebenen Reader in eine Liste von Listen von tabgetrennten
     * Werten ein und schließt den Reader dann.
     *
     * @param reader
     *            Zu verwendender Reader.
     * @return Liste von Listen von tabgetrennten Werten aus den Zeilen.
     */
    private static List<List<String>> readFromReaderToListOfFieldLines(Reader reader) {
        List<List<String>> listOfListOfFields = reader.readFileToListOfFieldLines();
        reader.close();
        return listOfListOfFields;
    }// TODO TESTEN

    /** Liest eine binäre Datei in ein Byte-Array ein. */
    public static byte[] readBinaryFileToByteArray(String fileName) {
        try {
            return tryToReadBinaryFileToByteArray(fileName);
        }
        catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        }
    }// TODO TESTEN

    private static byte[] tryToReadBinaryFileToByteArray(String fileName) throws IOException {
        File file = new File(fileName);
        InputStream insputStream = new FileInputStream(file);
        long length = file.length();
        byte[] bytes = new byte[(int) length];

        insputStream.read(bytes);
        insputStream.close();
        return bytes;
    }// TODO TESTEN

    /**
     * Liest die letzte nicht leere Zeile aus der Datei.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Encoding der Datei.
     * @return Inhalt der letzten nicht leeren Zeile aus der Datei oder der leere String.
     */
    public static String readLastNotEmptyLine(String filename) {
        List<String> list = readFileToList(filename);
        for (int index = list.size() - 1; index >= 0; --index) {
            String line = list.get(index);
            if (!line.isEmpty()) {
                return line;
            }
        }

        return "";
    }// TODO TESTEN

    /**
     * Speichert eine Datei als Sicherheitskopie ab. Der Name wird dabei automatisch erzeugt.
     *
     * Siehe auch copyFileToBackupIfExisting und moveFileToBackupIfExisting()!
     *
     * @param originalFileName
     *            Name der Originaldatei, die gesichert werden soll.
     */
    public static void backupFile(String originalFileName) {
        File originalFile = new File(originalFileName);
        if (originalFile.exists()) {
            long lastModified = originalFile.lastModified();
            GregorianCalendar cal = new GregorianCalendar();
            cal.setTime(new Date(lastModified));
            String insertion = String.format("_%1$tY%1$tm%1$td", cal);
            String backupFileName = insertBeforeExtension(originalFileName, insertion);

            copyFile(originalFileName, backupFileName);
        }
    }// TODO TESTEN

    /**
     * Fügt einen String in einen Dateinamen vor den Punkt und die Erweiterung. Also wird aus
     * "/user/bla/datei.txt" und "_foo" dann ein "/user/bla/datei_foo.txt".
     *
     * @param originalFileName
     *            Der Ausgangsname.
     * @param insertion
     *            Der einzufügende Teil.
     * @return Name mit eingefügtem Text.
     */
    public static String insertBeforeExtension(String originalFileName, String insertion) {
        String dir = FileHelper.getDirName(originalFileName);
        String bareName = FileHelper.getBareName(originalFileName);
        int lastPointIndex = bareName.lastIndexOf(".");
        String newBareName;
        if (lastPointIndex > -1) {
            String beforePoint = bareName.substring(0, lastPointIndex);
            String pointAndAfter = bareName.substring(lastPointIndex);
            newBareName = beforePoint + insertion + pointAndAfter;
        }
        else {
            newBareName = bareName + insertion;
        }
        if (dir.isEmpty()) {
            return newBareName;
        }
        else {
            return concatPathes(dir, newBareName);
        }
    }

    /**
     * Entfernt einen String in einen Dateinamen vor dem Punkt und der Erweiterung. Also wird aus
     * "/user/bla/datei_foo.txt" und "_foo" dann ein "/user/bla/datei.txt".
     *
     * @param originalFileName
     *            Der Ausgangsname.
     * @param toRemove
     *            Der einzufügende Teil.
     * @return Name mit eingefügtem Text.
     */
    public static String removeBeforeExtension(String originalFileName, String toRemove) {
        String dir = FileHelper.getDirName(originalFileName);
        String bareName = FileHelper.getBareName(originalFileName);
        int lastPointIndex = bareName.lastIndexOf(".");
        String newBareName;
        if (lastPointIndex > -1) {
            String beforePoint = bareName.substring(0, lastPointIndex);
            if (!beforePoint.endsWith(toRemove)) {
                throw new IllegalArgumentException("In '" + originalFileName + "' befindet sich kein '"
                        + toRemove + "' vor der Extension.");
            }
            String restAfterRemoving = beforePoint.substring(0,
                    beforePoint.length() - toRemove.length());
            String pointAndAfter = bareName.substring(lastPointIndex);
            newBareName = restAfterRemoving + pointAndAfter;
        }
        else {
            throw new IllegalArgumentException(
                    "In '" + originalFileName + "' gibt es keine Extension.");
        }

        if (dir.isEmpty()) {
            return newBareName;
        }
        else {
            return concatPathes(dir, newBareName);
        }
    }


    /**
     * Entfernt die Extension ("bla.txt" -> "bla", "c:/foo/bar.txt" -> "c:/foo/bar").
     *
     * @param originalFileName
     *            Der Ausgangsname.
     * @return Name ohne Extension.
     */
    public static String removeExtension(String originalFileName) {
        int lastPointIndex = originalFileName.lastIndexOf(".");
        if (lastPointIndex > -1) {
            return originalFileName.substring(0, lastPointIndex);
        }
        else {
            return originalFileName;
        }
    }

    /**
     * Entfernt alles ab dem letzten Unterstrich im Namen.
     *
     * @param originalFileName
     *            Der Ausgangsname.
     * @return Name ohne die Teile hinter dem letzten Slash.
     */
    public static String removeAllFromLastUnderline(String originalFileName) {
        int lastSlashIndex = originalFileName.lastIndexOf("_");
        if (lastSlashIndex > -1) {
            return originalFileName.substring(0, lastSlashIndex);
        }
        else {
            return originalFileName;
        }
    }

    /**
     * Ermittelt die Extension ("bla.txt" -> ".txt", "c:/foo/bar.foo" -> ".foo").
     *
     * @param originalFileName
     *            Der Ausgangsname.
     * @return Extension mit führendem Punkt.
     */
    public static String determineExtension(String originalFileName) {
        int lastPointIndex = originalFileName.lastIndexOf(".");
        if (lastPointIndex > -1) {
            return originalFileName.substring(lastPointIndex);
        }
        else {
            return NO_EXTENSION_FOUND;
        }
    }

    /**
     * Speichert einen String in eine Datei.
     *
     * @param text
     *            Der zu speichernde String.
     * @param filename
     *            Der Name der zu erzeugenden Datei.
     */
    public static void writeTextToFile(String text, String filename) {
        FineFileWriter writer = new FineFileWriter(filename);
        if (!text.isBlank()) {
            writer.writeln(text);
        }
        writer.close();
    }

    /**
     * Speichert einen String in eine Datei.
     *
     * @param text
     *            Der zu speichernde String.
     * @param filename
     *            Der Name der zu erzeugenden Datei.
     * @param charset
     *            Zeichensatz der zu schreibenden Datei.
     */
    public static void writeTextToFile(String text, String filename, Charset charset) {
        FineFileWriter writer = new FineFileWriter(filename, charset);
        writer.writeln(text);
        writer.close();
    }// TODO TESTEN

    /**
     * Speichert einen String in eine Datei mit aktuellem Datum und Zeit als Einschub vor die
     * Erweiterung.
     *
     * @param text
     *            Der zu speichernde String.
     * @param front
     *            Der vordere Teil des Namens der zu erzeugenden Datei.
     * @param rear
     *            Der hintere Teil des Namens der zu erzeugenden Datei.
     */
    public static void writeTextToFileWithDate(String text, String front, String rear) {
        GregorianCalendar cal = new GregorianCalendar();
        String insertion = String.format("%1$tY%1$tm%1$td_%1$tH%1$tM%1$tS", cal);
        writeTextToFile(text, front + insertion + rear);
    }// TODO TESTEN

    /**
     * Schreibt die Liste mit den Zeilen in die Datei mit dem angegebenen Namen.
     *
     * @param lines
     *            Die Liste mit den auszugebenden Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     */
    public static void writeLinesToFile(List<String> lines, String filename) {
        writeLinesToWriter(new FineFileWriter(filename), lines);
    }// TODO TESTEN

    /**
     * Schreibt die Liste mit den Zeilen unter Verwendung des angegebenen Encodings in die Datei
     * mit dem angegebenen Namen.
     *
     * @param lines
     *            Die Liste mit den auszugebenden Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @param charset
     *            Encoding der Datei.
     */
    public static void writeLinesToFile(List<String> lines, String filename, Charset charset) {
        writeLinesToWriter(new FineFileWriter(filename, charset), lines);
    }// TODO TESTEN

    /**
     * Schreibt den Text im Format "UCS-2 LE BOM".
     * Dann erkennt Notepad++ das auch als "UCS-2 LE BOM".
     *
     * @param lines
     *            Die Liste mit den auszugebenden Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @see http://stackoverflow.com/questions/17081953/write-text-file-in-ucs-2-little-endian-java
     */
    public static void writeLinesToUCS2LeBom(List<String> lines, String filename) {
        writeLinesToFileWithByteOrderMask(lines, filename, Charset.UTF_16LE, "\uFEFF");
    }// TODO TESTEN

    /**
     * Schreibt den Text im Format "UTF-8-BOM".
     * Dann erkennt Notepad++ das auch als "UTF-8-BOM".
     *
     * @param lines
     *            Die Liste mit den auszugebenden Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @see http://stackoverflow.com/questions/4389005/how-to-add-a-utf-8-bom-in-java
     */
    public static void writeLinesToUTF8Bom(List<String> lines, String filename) {
        writeLinesToFileWithByteOrderMask(lines, filename, Charset.UTF_8, "\uFEFF");
    }// TODO TESTEN

    /**
     * Schreibt die Liste mit den Zeilen unter Verwendung des angegebenen Encodings und der
     * angegebenen Byte-Order-Mask in die Datei mit dem angegebenen Namen.
     *
     * Dummer Weise ist die Datei nicht im richtigen Format, wenn man sie mit dem Charset.UTF_16LE,
     * also "UTF-16LE" bzw. "Sixteen-bit UCS Transformation Format, little-endian byte order")
     * schreibt.
     *
     * Laut http://stackoverflow.com/questions/17081953/write-text-file-in-ucs-2-little-endian-java
     * muss man am Anfang
     *     writer.write("\uFEFF");
     * ausführen. Das wird hier getan.
     *
     * @param lines
     *            Die Liste mit den auszugebenden Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @param charset
     *            Encoding der Datei.
     * @param byteOrderMask
     *            Zu schreibende Byte-Order-Mask.
     */
    public static void writeLinesToFileWithByteOrderMask(List<String> lines, String filename,
            Charset charset, String byteOrderMask) {
        Writer writer = new FineFileWriter(filename, charset);
        writer.write(byteOrderMask);
        writeLinesToWriter(writer, lines);
    }// TODO TESTEN

    /**
     * Schreibt die Zeilen auf den angegebenen Writer und schließt diesen dann.
     *
     * @param writer
     *            Zu verwendender Writer.
     * @param lines
     *            Auszugebende Zeilen.
     */
    private static void writeLinesToWriter(Writer writer, List<String> lines) {
        writer.writeAllLines(lines);
        writer.close();
    }// TODO TESTEN

    /**
     * Schreibt den Text im Format "UCS-2 LE BOM".
     * Dann erkennt Notepad++ das auch als "UCS-2 LE BOM".
     *
     * @param text
     *            Auszugebender Text.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @see http://stackoverflow.com/questions/17081953/write-text-file-in-ucs-2-little-endian-java
     */
    public static void writeTextToUCS2LeBom(String text, String filename) {
        writeTextToFileWithByteOrderMask(text, filename, Charset.UTF_16LE, "\uFEFF");
    }// TODO TESTEN

    /**
     * Schreibt den Text im Format "UTF-8-BOM".
     * Dann erkennt Notepad++ das auch als "UTF-8-BOM".
     *
     * @param text
     *            Auszugebender Text.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @see http://stackoverflow.com/questions/4389005/how-to-add-a-utf-8-bom-in-java
     */
    public static void writeTextToUTF8Bom(String text, String filename) {
        writeTextToFileWithByteOrderMask(text, filename, Charset.UTF_8, "\uFEFF");
    }// TODO TESTEN

    /**
     * Schreibt den Text unter Verwendung des angegebenen Encodings und der angegebenen
     * Byte-Order-Mask in die Datei mit dem angegebenen Namen.
     *
     * Dummer Weise ist die Datei nicht im richtigen Format, wenn man sie mit dem Charset.UTF_16LE,
     * also "UTF-16LE" bzw. "Sixteen-bit UCS Transformation Format, little-endian byte order")
     * schreibt.
     *
     * Laut http://stackoverflow.com/questions/17081953/write-text-file-in-ucs-2-little-endian-java
     * muss man am Anfang
     *     writer.write("\uFEFF");
     * ausführen. Das wird hier getan.
     *
     * @param text
     *            Der auszugebende Text.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @param charset
     *            Encoding der Datei.
     * @param byteOrderMask
     *            Zu schreibende Byte-Order-Mask.
     */
    public static void writeTextToFileWithByteOrderMask(String text, String filename,
            Charset charset, String byteOrderMask) {
        Writer writer = new FineFileWriter(filename, charset);
        writer.write(byteOrderMask);
        writer.writeln(text);
        writer.close();
    }// TODO TESTEN

    /**
     * Fügt den angegebenen Text und einen Zeilenumbruch an die angegebene Datei an.
     *
     * @param line
     *            Anzufügender Text.
     * @param filename
     *            Dateiname
     */
    public static void appendLineToFile(String line, String filename) {
        Writer writer = new FineFileWriter(filename, true);
        writer.writeln(line);
        writer.close();
    }// TODO TESTEN

    /**
     * Fügt den angegebenen Text und einen Zeilenumbruch an die angegebene Datei an.
     *
     * @param line
     *            Anzufügender Text.
     * @param filename
     *            Dateiname
     * @param charset
     *            Zu verwendendes Charset.
     */
    public static void appendLineToFile(String line, String filename, Charset charset) {
        Writer writer = new FineFileWriter(filename, charset, true);
        writer.writeln(line);
        writer.close();
    }// TODO TESTEN

    /**
     * Fügt die angegebenen Zeilen mit je einem Zeilenumbruch an die angegebene Datei an.
     *
     * @param lines
     *            Anzufügende Zeilen.
     * @param filename
     *            Dateiname
     * @param charset
     *            Zu verwendendes Charset.
     */
    public static void appendLinesToFile(List<String> lines, String filename, Charset charset) {
        Writer writer = new FineFileWriter(filename, charset, true);
        for (String line : lines) {
            writer.writeln(line);
        }
        writer.close();
    }// TODO TESTEN

    /**
     * Verschiebt eine Datei (mit Hilfe von {@link File#renameTo(File)}).
     *
     * @param source
     *            Die Datei, die kopiert werden soll.
     * @param target
     *            Die Datei, die erstellt und geschrieben wird.
     * @return Erfolg
     */
    public static boolean moveFileByRenameTo(String source, String target) {
        if (source.equals(target)) {
            return false;
        }

        File sourceFile = new File(source);
        File targetFile = new File(target);
        return sourceFile.renameTo(targetFile);
    }// TODO TESTEN

    /**
     * Verschiebt eine Datei (mit Hilfe von {@link Files#move(String)}).
     *
     * @param source
     *            Die Datei, die kopiert werden soll.
     * @param target
     *            Die Datei, die erstellt und geschrieben wird.
     * @return Erfolg
     */
    public static boolean moveFileByFilesMove(String source, String target) {
        if (source.equals(target)) {
            return false;
        }
        Path fileToMovePath = Paths.get(source);
        Path targetPath = Paths.get(target);
        try {
            Files.move(fileToMovePath, targetPath);
            return true;
        }
        catch (IOException e) {
            return false;
        }
    }

    /**
     * Kopiert den Text aus einer Datei in eine andere.
     *
     * @param source
     *            Die Datei, die kopiert werden soll.
     * @param target
     *            Die Datei, die erstellt und geschrieben wird.
     */
    public static void copyTextFileByReadAndWrite(String source, String target) {
        if (source.equals(target)) {
            return;
        }

        Reader reader = new FineFileReader(source);
        Writer writer = new FineFileWriter(target);
        while (true) {
            String line = reader.readNextLine();
            if (null == line) {
                break;
            }
            writer.writeln(line);
        }
        writer.close();
        reader.close();
    }// TODO TESTEN

    /**
     * Setzt zwei Pfade so zusammen, dass sowohl doppelte als auch fehlende Trenner vermieden
     * werden.
     *
     * @param part1
     *            Erster Teil des Pfades.
     * @param part2
     *            Zweiter Teil des Pfades.
     * @return Zusammengesetzter Pfad.
     */
    public static String concatPathes(String part1, String part2) {
        boolean endsWithSlash = part1.endsWith("/") || part1.endsWith("\\");
        boolean startsWithSlash = part2.startsWith("/") || part2.startsWith("\\");

        if (endsWithSlash && startsWithSlash) {
            String tmp = part1.substring(0, part1.length() - 1);
            return tmp + part2;
        }
        else if (endsWithSlash || startsWithSlash) {
            return part1 + part2;
        }
        else {
            return part1 + File.separator + part2;
        }
    }

    /**
     * Setzt zwei Pfade so zusammen, dass sowohl doppelte als auch fehlende Trenner vermieden
     * werden.
     *
     * @param part1
     *            Erster Teil des Pfades.
     * @param part2
     *            Zweiter Teil des Pfades.
     * @return Zusammengesetzter Pfad.
     */
    public static String concatPathes(File part1, String part2) {
        return concatPathes(part1.getPath(), part2);
    }// TODO TESTEN

    /**
     * Setzt zwei Pfade so zusammen, dass sowohl doppelte als auch fehlende Trenner vermieden
     * werden.
     *
     * @param part1
     *            Erster Teil des Pfades.
     * @param part2
     *            Zweiter Teil des Pfades.
     * @return Zusammengesetzter Pfad.
     */
    public static String concatPathes(String part1, File part2) {
        return concatPathes(part1, part2.getPath());
        /*
         * Macht das Sinn? Dann würde man an irgendwas einen kompletten Pfad dranhängen, vermutlich
         * nicht! Momentan wird die Methode auch nicht genutzt.
         */
    }// TODO TESTEN

    /**
     * Setzt zwei Pfade so zusammen, dass sowohl doppelte als auch fehlende Trenner vermieden
     * werden.
     *
     * @param part1
     *            Erster Teil des Pfades.
     * @param part2
     *            Zweiter Teil des Pfades.
     * @return Zusammengesetzter Pfad.
     */
    public static String concatPathes(File part1, File part2) {
        return concatPathes(part1.getPath(), part2.getPath());
        /*
         * Macht das Sinn? Dann würde man an einen kompletten Pfad einen kompletten Pfad
         * dranhängen, vermutlich nicht! Momentan wird die Methode auch nicht genutzt.
         */
    }// TODO TESTEN

    /**
     * Setzt mehrere Pfadstücke so zusammen, dass sowohl doppelte als auch fehlende Trenner
     * vermieden werden.
     *
     * @param parts
     *            Teile des Pfades
     * @return Zusammengesetzter Pfad.
     */
    public static String concatPathesMultiple(String ... parts) {
        if (parts.length == 0) {
            return "";
        }
        else if (parts.length == 1) {
            return parts[0];
        }

        String filename = parts[0];
        for (int index = 1; index < parts.length; ++index) {
            filename = concatPathes(filename, parts[index]);
        }

        return filename;
    }

    /**
     * Erzeugt aus einer Zeile der Art                          <br><br>&nbsp;&nbsp;&nbsp;&nbsp;<tt>
     *     "DokNr";"Vortext"                                    </tt><br><br>
     * eine Liste mit den einzelnen Werten.
     *
     * @param line
     *            Eingelesene Zeile.
     * @return Liste mit den Werten der Zeile.
     */
    public static List<String> splitCsvSemikolonLine(String line) {
        if (!line.startsWith("\"")) {
            throw new IllegalArgumentException("Die zum Aufsplitten übergebene Zeile beginnt "
                    + "nicht mit einem Anführungzeichen!\n\tZeile: " + line);
        }// TODO TESTEN
        if (!line.endsWith("\"")) {
            throw new IllegalArgumentException("Die zum Aufsplitten übergebene Zeile wird nicht "
                    + "von einem Anführungzeichen beendet!\n\tZeile: " + line);
        }// TODO TESTEN

        /* Vorderes und hinteres Anführungszeichen abschneiden: */
        String shortLine = line.substring(1, line.length() - 1);

        return Arrays.asList(shortLine.split("\";\"", -1));
    }

    /**
     * Ermittelt alle Dateien ohne Pfad in einem Verzeichnis, die einem bestimmten Kriterium
     * entsprechen.
     *
     * @param source
     *            Verzeichnis, in dem gesucht wird.
     * @param filenameFilter
     *            Kriterium an den Dateinamen der zu suchenden Dateien.
     * @return Liste mit allen passenden Dateien.
     */
    public static List<String> findInDirWithoutPath(String source, FilenameFilter filenameFilter) {
        List<String> list = findInDir(source, filenameFilter);
        return removePath(list);
    }// TODO TESTEN

    private static List<String> removePath(List<String> list) {
        List<String> result = new ArrayList<>();
        for (String file : list) {
            String fileWithoutPath = getBareName(file);
            result.add(fileWithoutPath);
        }
        return result;
    }

    /**
     * Ermittelt alle Dateien in einem Verzeichnis, die einem bestimmten Kriterium entsprechen.
     *
     * @param source
     *            Verzeichnis, in dem gesucht wird.
     * @param filenameFilter
     *            Kriterium an den Dateinamen der zu suchenden Dateien.
     * @return Liste mit allen passenden Dateien.
     */
    public static List<String> findInDir(String source, FilenameFilter filenameFilter) {
        return findInDir(new File(source), filenameFilter);
    }// TODO TESTEN

    /**
     * Ermittelt alle Dateien in einem Verzeichnis, die einem bestimmten Kriterium entsprechen.
     *
     * @param source
     *            Verzeichnis, in dem gesucht wird.
     * @param filenameFilter
     *            Kriterium an den Dateinamen der zu suchenden Dateien.
     * @return Liste mit allen passenden Dateien.
     */
    public static List<String> findInDir(File source, FilenameFilter filenameFilter) {
        List<String> files = new ArrayList<>();

        for (File file : source.listFiles(filenameFilter)) {
            files.add(file.getPath());
        }

        return files;
    }// TODO TESTEN

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param filename
     *            Name der Datei.
     * @return Zeitpunkt als DateAndTime.
     */
    public static DateAndTime fileLastModifiedAsDateAndTime(String filename) {
        String dateAndTimeString = fileLastModified(filename);
        String lastModifiedDateYyyyMmDd = dateAndTimeString.substring(0, 8);
        ImmutualDate lastModifiedDate = new ImmutualDate(lastModifiedDateYyyyMmDd);

        String lastModifiedDateHhMmSs = dateAndTimeString.substring(9);
        if (lastModifiedDateHhMmSs.length() != 6) {
            throw new RuntimeException("Die ermittelte Zeit hat nicht sechs Stellen\n"
                    + "\t" + "filename                 = '" + filename + "'\n"
                    + "\t" + "dateAndTimeString        = '" + dateAndTimeString + "'\n"
                    + "\t" + "lastModifiedDateYyyyMmDd = '" + lastModifiedDateYyyyMmDd + "'\n"
                    + "\t" + "lastModifiedDate         = '" + lastModifiedDate + "'\n"
                    + "\t" + "lastModifiedDateHhMmSs   = '" + lastModifiedDateHhMmSs + "'\n"
                    );
        }
        lastModifiedDateHhMmSs = ""
                + lastModifiedDateHhMmSs.substring(0, 2)
                + ":"
                + lastModifiedDateHhMmSs.substring(2, 4)
                + ":"
                + lastModifiedDateHhMmSs.substring(4, 6)
                ;
        ImmutualTime lastModifiedTime = new ImmutualTime(lastModifiedDateHhMmSs);
        return new DateAndTime(lastModifiedDate, lastModifiedTime);
    }

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param filename
     *            Name der Datei.
     * @return Zeitpunkt als DateAndTimeString.
     */
    public static DateAndTimeString fileLastModifiedAsDateAndTimeString(String filename) {
        String timestamp = FileHelper.fileLastModified(filename); // Format YYYYMMDD_HHMMSS
        int underscoreIndex = timestamp.indexOf("_");
        if (underscoreIndex == -1) {
            throw new RuntimeException("Keinen Unterstrich im Timestamp gefunden, den "
                    + "FileHelper.fileLastModified() liefert.\n"
                    + "\t" + "filename : " + filename + "\n"
                    + "\t" + "timestamp: " + timestamp + "\n");
        }
        String date = timestamp.substring(0, underscoreIndex);  // Format YYYYMMDD
        String time = timestamp.substring(underscoreIndex + 1); // Format HHMMSS

        ImmutualDate immutualDate = DateHelper.getDateFromYYYYMMDD(date);
        date = immutualDate.toString(); // Format DD.MM.YYYY

        time = TimeHelper.insertTimeColonsHHMMSS(time); // Format HH:MM:SS

        return new DateAndTimeString(date, time);
    }

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param filename
     *            Name der Datei.
     * @return Zeitpunkt im Timestamp-Format YYYYMMDD_HHMMSS.
     */
    public static String fileLastModified(String filename) {
        return fileLastModified(new File(filename));
    }

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param file
     *            Zu untersuchende Datei.
     * @return Zeitpunkt im Timestamp-Format YYYYMMDD_HHMMSS.
     */
    public static String fileLastModified(File file) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
        return sdf.format(fileLastModifiedAsMillis(file));
    }

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param filename
     *            Name der Datei.
     * @return Zeitpunkt als Millisekunden.
     */
    public static long fileLastModifiedAsMillis(String filename) {
        return fileLastModifiedAsMillis(new File(filename));
    }// TODO TESTEN

    /**
     * Ermittelt den letzten Zeitpunkt der Modifikation einer Datei.
     *
     * @param file
     *            Zu untersuchende Datei.
     * @return Zeitpunkt als Millisekunden.
     */
    public static long fileLastModifiedAsMillis(File file) {
        return file.lastModified();
    }

    /**
     * Setzt den Zeitpunkt der letzten Modifikation der angegebenen Datei.
     *
     * @param filename
     *            Der Name der zu modifizierenden Datei.
     * @param date
     *            Das Datum im Format "DD.MM.YYYY".
     * @param time
     *            Die Uhrzeit im Format "HH:MM:SS"
     */
    public static void setLastModified(String filename, String date, String time) {
        long lastModified = dateAndTimeToMillisecondsSinceEpoche(date, time);
        setLastModified(filename, lastModified);
    }// TODO TESTEN

    /**
     * Erzeugt auf Datum und Uhrzeit die Millisekunden seit Epoche (01.01.1970).
     *
     * @param date
     *            Das Datum im Format "DD.MM.YYYY".
     * @param time
     *            Die Uhrzeit im Format "HH:MM:SS"
     * @return Die Millisekunden seit Epoche (01.01.1970).
     */
    public static long dateAndTimeToMillisecondsSinceEpoche(String date, String time) {
        try {
            return tryToDateAndTimeToMillisecondsSinceEpoche(date, time);
        }
        catch (Exception exception) {
            throw new RuntimeException(exception);
        }
    }// TODO TESTEN

    private static long tryToDateAndTimeToMillisecondsSinceEpoche(String date, String time)
            throws ParseException {
        String dateAndTime = date + " " + time;
        SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
        Date internalDate = format.parse(dateAndTime);
        long timeInMillis = internalDate.getTime();
        return timeInMillis;
    }

    /**
     * Setzt den Zeitpunkt der letzten Modifikation der angegebenen Datei.
     *
     * @param filename
     *            Der Name der zu modifizierenden Datei.
     * @param lastModified
     *            Der zu setzende Zeitpunkt in Millisekunden seit Epoche (01.01.1970).
     */
    public static void setLastModified(String filename, long lastModified) {
        setLastModified(new File(filename), lastModified);
    }// TODO TESTEN

    /**
     * Setzt den Zeitpunkt der letzten Modifikation der angegebenen Datei.
     *
     * @param file
     *            Die zu modifizierende Datei.
     * @param lastModified
     *            Der zu setzende Zeitpunkt in Millisekunden seit Epoche (01.01.1970).
     */
    private static void setLastModified(File file, long lastModified) {
        if (file.isFile()) {
            file.setLastModified(lastModified);
        }
    }// TODO TESTEN

    /**
     * Setzt den Zeitpunkt der Erstellung, der letzten Modifikation und des letzten Zugriffs der
     * angegebenen Datei.
     *
     * @param filename
     *            Der Name der zu modifizierenden Datei.
     * @param date
     *            Das Datum im Format "DD.MM.YYYY".
     * @param time
     *            Die Uhrzeit im Format "HH:MM:SS"
     */
    public static void setCreationLastModifiedAndLastAccessTime(String filename, String date,
            String time) {
        long milliseconds = dateAndTimeToMillisecondsSinceEpoche(date, time);
        setCreationLastModifiedAndLastAccessTime(filename, milliseconds);
    }// TODO TESTEN

    /**
     * Setzt den Zeitpunkt der Erstellung, der letzten Modifikation und des letzten Zugriffs der
     * angegebenen Datei.
     *
     * @param filename
     *            Der Name der Datei, deren Zeitpunkt der Erstellung, der letzten Modifikation und
     *            des letzten Zugriffs verändert werden soll.
     * @param milliseconds
     *            Der zu setzende Zeitpunkt in Millisekunden seit Epoche (01.01.1970).
     */
    public static void setCreationLastModifiedAndLastAccessTime(String filename, long milliseconds) {
        try {
            tryToSetCreationLastModifiedAndLastAccessTime(filename, milliseconds);
        }
        catch (Exception exception) {
            throw new RuntimeException("Die Zeitpunkte der Erstellung, der letzten Modifikation "
                    + "und des letzten Zugriffs der Datei konnten nicht gesetzt werden.\n"
                    + "\t" + "filename = " + filename + "\n"
                    + "\t" + "milliseconds = " + milliseconds + "\n",
                    exception);
        }
    }// TODO TESTEN

    private static void tryToSetCreationLastModifiedAndLastAccessTime(String filename,
            long milliseconds) throws IOException {
        BasicFileAttributeView attributes = Files.getFileAttributeView(
                Paths.get(filename), BasicFileAttributeView.class);
        FileTime time = FileTime.fromMillis(milliseconds);
        attributes.setTimes(time, time, time);
    }

    /**
     * Stellt fest, ob das angegebene Verzeichnis leer ist.
     *
     * @param dir
     *            Zu überprüfendes Verzeichnis, muss existieren.
     * @return Wahrheitswert: true genau dann, wenn das Verzeichnis leer ist.
     */
    public static boolean isDirectoryEmpty(String dir) {
        File dirAsFile = new File(dir);
        int numberOfFoundFiles = dirAsFile.listFiles().length;
        return 0 == numberOfFoundFiles;
    }

    /**
     * Stellt fest, ob das angegebene Verzeichnis ein Verzeichnis und leer ist.
     *
     * @param dir
     *            Zu überprüfendes Verzeichnis, muss existieren.
     * @return Wahrheitswert: true genau dann, wenn das Verzeichnis ein Verzeichnis und leer ist.
     */
    public static boolean isEmptyDirectory(String dir) {
        return isDirectory(dir) && isDirectoryEmpty(dir);
    }

    /**
     * Prüft ob die übergebene Datei leer ist.
     *
     * @param filename
     *            Name der übergebenen Datei.
     */
    public static boolean isFileEmpty(String filename) {
        String content = readFileToString(filename);
        return content.isEmpty();
    }

    /**
     * Prüft ob die übergebene Datei existiert und leer ist.
     *
     * @param filename
     *            Name der übergebenen Datei.
     */
    public static boolean isEmptyFile(String filename) {
        return isFile(filename) && isFileEmpty(filename);
    }

    /**
     * Stellt fest, ob das angegebenen Verzeichnis Unterverzeichnisse enthält.
     *
     * @param dir
     *            Zu überprüfendes Verzeichnis, muss existieren.
     * @return Wahrheitswert: true genau dann, wenn das Verzeichnis Unterverzeichnisse enthält.
     */
    public static boolean hasDirectorySubdirectories(String dir) {
        if (!isDirectory(dir)) {
            return false;
        }
        File dirAsFile = new File(dir);
        for (File file : dirAsFile.listFiles()) {
            if (file.isDirectory()) {
                return true;
            }
        }
        return false;
    }// TODO TESTEN

    /**
     * Erstellt ein leere Datei im Default-Verzeichnis für temporäre Dateien, die mit dem Prefix
     * beginnt und mit dem Suffix endet.
     *
     * @param prefix
     *            Prefix mit dem der Dateiname beginnt. Muss mindestens drei Zeichen lang sein.
     * @param suffix
     *            Suffix auf den der Dateiname endet.
     * @return File-Objekt mit der erstellten Datei.
     * @throws IORuntimeException
     *             Falls etwas dabei schief geht.
     */
    public static File createTempFile(String prefix, String suffix) {
        try {
            return File.createTempFile(prefix, suffix);
        }
        catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }// TODO TESTEN

    /**
     * Entfernt . und .. aus Dateinamen.
     *
     * @param filename
     *            Dateiname.
     * @return normalisierter Dateiname.
     */
    public static String normalisePath(String filename) {
        try {
            File file = new File(filename);
            return file.getCanonicalPath();
        }
        catch (IOException e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Ermittelt die Laufwerksbezeichnung zu einer beliebigen Datei oder einem Verzeichnis.
     *
     * @param path
     *            Dateiname oder Verzeichnisname mit vollem Pfad.
     * @return Laufwerksbezeichnung
     * @throws RuntimeException
     *             Falls path nicht existiert oder zu path die Laufwerksbezeichnung nicht ermittelt
     *             werden konnte (z.B. wenn path nicht mit "R:\\" oder "r:\\" oder so beginnt,
     *             sondern mit "R:/" (hier hilft normalizeToBackSlashes()) oder
     *             "\\Cluster1\Projekte1").
     */
    public static String getSystemDisplayName(String path) {
        File file = new File(path);
        if (!file.exists()) {
            throw new RuntimeException("Die Datei oder das Verzeichnis '" + path
                    + "' existiert nicht!");
        }// TODO TESTEN

        FileSystemView fsv = FileSystemView.getFileSystemView();
        String upperCasePath = path.toUpperCase();
        for (File rootAsFile : File.listRoots()) {
            String root = rootAsFile.getPath();
            String upperCaseRoot = root.toUpperCase();
            if (upperCasePath.startsWith(upperCaseRoot)) {
                String displayName = fsv.getSystemDisplayName(rootAsFile);
                if (displayName.matches(".* \\([A-Z]:\\)")) {
                    displayName = displayName.substring(0, displayName.length() - 5);
                }
                return displayName;
            }
        }

        throw new RuntimeException("Zur Datei oder das Verzeichnis '" + path
                    + "' konnte die Laufwerksbezeichnung nicht ermittelt werden!");
    }

    /**
     * Macht aus einem Pfad der Art "C:\Temp" einen String, der als Name einer Datei gültig ist.
     * Im Beispiel "c_Temp".
     *
     * @param path Pfadangabe mit Verzeichnistrennern, Laufwerksbuchstaben etc.
     * @return Name der als Dateiname (ohne Pfad) gültig wäre.
     */
    public static String buildFilenameFromPath(String path) {
        String file = path;

        file = file.replace(":", "");
        file = file.replace("\\", "_");
        file = file.replace("/", "_");
        file = file.replace(";", "_");
        file = file.replaceAll("^_+", "");

        return file;
    }

    /**
     * Ermittelt die Länge der Datei in Bytes.
     *
     * @param file
     *            Datei.
     * @return Länge der Datei in Bytes.
     * @throws IORuntimeException
     *             Falls die Datei nicht existiert oder ein Verzeichnis ist.
     */
    public static long fileSize(File file) {
        if (!file.isFile()) {
            throw new IORuntimeException("Die Datei '" + file.getPath()
                    + "', deren Länge ermittelt wird, ist keine Datei im Dataeisystem!");
        }
        return file.length();
    }

    /**
     * Ermittelt die Länge der Datei in Bytes.
     *
     * @param filename
     *            Name der Datei mit Pfad.
     * @return Länge der Datei in Bytes.
     * @throws IORuntimeException
     *             Falls die Datei nicht existiert oder ein Verzeichnis ist.
     */
    public static long fileSize(String filename) {
        return fileSize(new File(filename));
    }

    /** Ermittelt den freien Platz auf der Festplatte oder Partition, auf der die Datei liegt. */
    public static long freeDiscSpace(String filename) {
        return freeDiscSpace(new File(filename));
    }// TODO TESTEN

    /** Ermittelt den freien Platz auf der Festplatte oder Partition, auf der die Datei liegt. */
    public static long freeDiscSpace(File file) {
        return file.getUsableSpace();
    }// TODO TESTEN

    /** Ermittelt den gesamten Platz auf der Festplatte oder Partition, auf der die Datei liegt. */
    public static long totalDiscSpace(String filename) {
        return totalDiscSpace(new File(filename));
    }// TODO TESTEN

    /** Ermittelt den gesamten Platz auf der Festplatte oder Partition, auf der die Datei liegt. */
    public static long totalDiscSpace(File file) {
        return file.getTotalSpace();
    }// TODO TESTEN

    /**
     * Prüft ob zwei Dateien die gleiche Last-Modified-Time und die gleiche Dateigröße haben.
     *
     * @param fileA
     *            Datei 1.
     * @param fileB
     *            Datei 2.
     * @return Wahrheitswert.
     */
    public static boolean filesAreEqualEnough(File fileA, File fileB) {
        return filesHaveSameLastModifiedTime(fileA, fileB)
                && filesHaveEqualSize(fileA, fileB);
    }

    /**
     * Prüft ob zwei Dateien die gleiche Last-Modified-Time und die gleiche Dateigröße haben.
     *
     * @param filenameA
     *            Name der ersten Datei.
     * @param filenameB
     *            Name der zweiten Datei.
     * @return Wahrheitswert.
     */
    public static boolean filesAreEqualEnough(String filenameA, String filenameB) {
        return filesAreEqualEnough(new File(filenameA), new File(filenameB));
    }

    /**
     * Prüft ob zwei Dateien die gleiche Last-Modified-Time haben.
     *
     * @param fileA
     *            Datei 1.
     * @param fileB
     *            Datei 2.
     * @return Wahrheitswert.
     */
    public static boolean filesHaveSameLastModifiedTime(File fileA, File fileB) {
        String lastModifiedA = fileLastModified(fileA);
        String lastModifiedB = fileLastModified(fileB);
        return lastModifiedA.equals(lastModifiedB);
    }

    /**
     * Prüft ob zwei Dateien die gleiche Last-Modified-Time haben.
     *
     * @param filenameA
     *            Name der ersten Datei.
     * @param filenameB
     *            Name der zweiten Datei.
     * @return Wahrheitswert.
     */
    public static boolean filesHaveSameLastModifiedTime(String filenameA, String filenameB) {
        return filesHaveSameLastModifiedTime(new File(filenameA), new File(filenameB));
    }

    /**
     * Prüft ob zwei Dateien die gleiche gleiche Dateigröße haben.
     *
     * @param fileA
     *            Datei 1.
     * @param fileB
     *            Datei 2.
     * @return Wahrheitswert.
     */
    public static boolean filesHaveEqualSize(File fileA, File fileB) {
        long sizeA = fileSize(fileA);
        long sizeB = fileSize(fileB);
        return sizeA == sizeB;
    }

    /**
     * Prüft ob zwei Dateien die gleiche gleiche Dateigröße haben.
     *
     * @param filenameA
     *            Name der ersten Datei.
     * @param filenameB
     *            Name der zweiten Datei.
     * @return Wahrheitswert.
     */
    public static boolean filesHaveEqualSize(String filenameA, String filenameB) {
        return filesHaveEqualSize(new File(filenameA), new File(filenameB));
    }

    /**
     * Tauscht die Erweiterung eines Dateinamens aus.
     *
     * Falls die Endung nicht ermittelt werden kann oder der erzeugte Dateiname dem originalen
     * gleicht, wird eine Ausnahme geworfen.
     *
     * @param filename
     *            Der ursprüngliche Dateiname dessen Erweiterung ausgetauscht werden soll.
     * @param newExtension
     *            Die neue Erweiterung (sie kann mit oder ohne führenden Punkt angegeben werden).
     * @return Der neue Dateiname.
     */
    public static String exchangeExtension(String filename, String newExtension) {
        int lastPointIndex = filename.lastIndexOf(".");
        if (0 > lastPointIndex) {
            throw new IllegalArgumentException("Der Dateiname '" + filename
                    + "' enthält keinen Punkt.");
        }
        if (0 == lastPointIndex) {
            throw new IllegalArgumentException("Der Dateiname '" + filename
                    + "' startet mit einem Punkt und das ist auch der einzige Punkt.");
        }

        String possibleOldExtension = filename.substring(lastPointIndex + 1);
        if (possibleOldExtension.isEmpty()) {
            throw new IllegalArgumentException("Der Dateiname '" + filename
                    + "' endet auf einen Punkt.");
        }

        if (Text.containsOnlyNormalLettersOrDigits(possibleOldExtension)) {
            String partWithoutExtension = filename.substring(0, lastPointIndex);
            String newFilename;
            if (newExtension.startsWith(".")) {
                newFilename = partWithoutExtension + newExtension;
            }
            else {
                newFilename = partWithoutExtension + "." + newExtension;
            }
            if (filename.equals(newFilename)) {
                throw new IllegalArgumentException("Der neue Dateiname ist gleich dem übergebenen "
                        + "Dateinamen!\n\t"
                        + "filename     = " + filename + "\n\t"
                        + "newExtension = " + newExtension + "\n\t"
                        + "newFilename  = " + newFilename);
            }
            else {
                return newFilename;
            }
        }
        else {
            throw new IllegalArgumentException("Die Endung '" + possibleOldExtension
                    + "' des Dateinamens '" + filename + "' ist seltsam.");
        }
    }

    /**
     * Ersetzt einige UNC-Pfade durch die Laufwerksbuchstaben, da sonst Lynx damit nicht klarkommt.
     *
     * @param path
     *            Zu bearbeitender Pfad.
     * @return Bearbeiteter Pfad.
     */
    public static String replaceUncPathes(String path) {
        String replacedPath = path;

        replacedPath = replacedPath.replace("\\\\Cluster1\\user1\\CD", "u:");
        replacedPath = replacedPath.replace("\\\\Cluster1\\User1\\CD", "u:");

        replacedPath = replacedPath.replace("\\\\Cluster2\\Projekte2\\Schufa\\Impressum", "i:");
        replacedPath = replacedPath.replace("\\\\Cluster1\\Projekte1", "r:");
        replacedPath = replacedPath.replace("\\\\Cluster2\\Projekte2", "s:");

        return replacedPath;
    }

    /**
     * Liest eine Datei ein und entfernt die Einrückungen, die Lynx erzeugt.
     *
     * @param filename
     *            Name der einzulesenden Datei.
     * @param charset
     *            Das Charset der einzulesenden Datei.
     * @return String mit dem eingelesenen Inhalt der Datei.
     */
    public static String readAndRemoveIndentation(String filename, Charset charset) {
        Reader reader = new FineFileReader(filename, charset);
        StringBuilder builder = new StringBuilder();

        String line = reader.readNextLine();
        while (line != null) {
            builder.append(line.trim());
            builder.append(Text.LINE_BREAK);
            line = reader.readNextLine();
        }

        reader.close();
        return builder.toString();
    }// TODO TESTEN

    /** Löscht alle Dateien und Unterverzeichnisse im angegebenen Verzeichnis. */
    public static void cleanDirectory(String directory) {
        deleteTree(directory);
        createDirectory(directory);
    }// TODO TESTEN

    /**
     * Erstellt eine geordnete Liste aller Dateien in einem Verzeichnis (ohne Unterverzeichnisse zu
     * beachten), die Dateien sind dann absteigend nach dem letzten Zeitpunkt der Modifikation
     * einer Datei sortiert.
     *
     * @param dir
     *            Verzeichnis dessen Dateien sortiert zurückgegeben werden sollen.
     * @return Absteigend sortierte Liste der Dateien.
     */
    public static List<FileWithModificationTime> determineAllFilesInMainDirectoryDescendingByTime(
            String dir) {
        List<String> files = findAllFilesInMainDirectoryNio2(dir);
        List<FileWithModificationTime> fileWithModificationTimes = new ArrayList<>();

        for (String file : files) {
            FileWithModificationTime fileWithModificationTime = new FileWithModificationTime(file);
            fileWithModificationTimes.add(fileWithModificationTime);
        }

        Collections.sort(fileWithModificationTimes, new Comparator<FileWithModificationTime>() {
            @Override
            public int compare(FileWithModificationTime file1, FileWithModificationTime file2) {
                String modified1 = file1.getModificationTime();
                String modified2 = file2.getModificationTime();

                int cmp = modified2.compareTo(modified1);
                if (cmp == 0) {
                    String filename1 = file1.getFilename();
                    String filename2 = file2.getFilename();
                    cmp = filename1.compareTo(filename2);
                }
                return cmp;
            }
        });

        return fileWithModificationTimes;
    }

    public static String prepareToFilenamePart(String input) {
        String filenamePart = input;

        filenamePart = filenamePart.replaceAll("[^_.A-Za-z0-9ÄÖÜäöüß-]", "_");
        filenamePart = filenamePart.replaceAll("__+", "_");
        filenamePart = filenamePart.replaceAll("^_+", "");
        filenamePart = filenamePart.replaceAll("_+$", "");

        return filenamePart;
    }

    /** Legt eine Datei leer an oder überschreibt eine vorhandene Datei leer. */
    public static void clearFile(String filename, Charset charset) {
        Writer writer = new FineFileWriter(filename, charset);
        writer.close();
    }// TODO TESTEN

//    /**
//     * Die Methode liest die erste Zeile der Datei ein.
//     *
//     * @param filename
//     *            Name der Datei.
//     * @param charset
//     *            Kodierung der Datei.
//     * @return Erste Zeile. Falls es keine erste Zeile gibt, wird der leere String zurück gegeben.
//     */
//    public static String readFirstLine(String filename, Charset charset) {
//        Reader reader = new FineFileReader(filename, charset);
//
//        String line = reader.readNextLine();
//        if (null == line) {
//            line = "";
//        }
//
//        reader.close();
//
//        return line;
//    }// TODO TESTEN

    /**
     * Die Methode liest die erste Zeile der Datei im Encoding ISO_8859_1 ein.
     *
     * @param filename
     *            Name der Datei.
     * @return Erste Zeile. Falls es keine erste Zeile gibt, wird eine Ausnahme geworfen.
     */
    public static String readFirstLine(String filename) {
        return readFirstLine(filename, Charset.ISO_8859_1);
    }// TODO TESTEN

    /**
     * Die Methode liest die erste Zeile der Datei ein.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Kodierung der Datei.
     * @return Erste Zeile. Falls es keine erste Zeile gibt, wird eine Ausnahme geworfen.
     */
    public static String readFirstLine(String filename, Charset charset) {
        Reader reader = new FineFileReader(filename, charset);

        String line = reader.readNextLine();
        if (line == null) {
            throw new RuntimeException("Die Datei '" + filename + "' ist leer.");
        }

        reader.close();

        return line;
    }// TODO TESTEN

    /**
     * Die Methode liest die Datei soweit, bis sie auf eine Zeile mit dem gewünschten Anfang stößt.
     * Dann wird diese Zeile zurückgegeben.
     *
     * Wird keine entsprechende Zeile gefunden, wird der leere String zurückgegeben.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Kodierung der Datei.
     * @param lineStart
     *            Anfang der gesuchten Zeile.
     * @return Erste Zeile, die mit dem gesuchten Anfang beginnt oder der leere String, falls keine
     *         solche gefunden werden konnte.
     */
    public static String readFirstLineStartingWith(String filename, Charset charset,
            String lineStart) {
        Reader reader = new FineFileReader(filename, charset);

        boolean found = false;

        String line = ""; // Initialisierung wegen "return line" ...
        while (!found && null != (line = reader.readNextLine())) {
            if (line.startsWith(lineStart)) {
                found = true;
            }
        }

        reader.close();

        if (found) {
            return line;
        }
        else {
            return "";// TODO TESTEN
        }
    }

    /**
     * Kopiert die ersten n Zeilen einer Textdatei.
     *
     * Hat die zu kopierende Datei höchstens n Zeilen, wird sie ganz kopiert.
     *
     * @param sourceFilename
     *            Name der Eingabedatei.
     * @param targetFilename
     *            Name der Ausgabedatei.
     * @param charset
     *            Kodierung der Dateien.
     * @param numberOfLinesToCopy
     *            Anzahl zu kopierender Zeilen.
     */
    public static void copyFirstNLinesOfTextFile(String sourceFilename,
            String targetFilename, Charset charset, int numberOfLinesToCopy) {
        Writer writer = new FineFileWriter(targetFilename, charset);
        Reader reader = new FineFileReader(sourceFilename, charset);

        String line;
        int count = 0;
        while (null != (line = reader.readNextLine())) {
            if (++count > numberOfLinesToCopy) {
                break;
            }
            writer.writeln(line);
        }

        reader.close();
        writer.close();
    }

    /**
     * Kopiert eine Datei auf einen Backupnamen.
     *
     * Wenn es diese schon gibt, dann wird der Backupzähler erhöht und so weiter, bis die Datei
     * nicht existiert.
     *
     * @param filename
     *            Name der Datei.
     * @param insertTextBeforeExtension
     *            Einschub vor die Erweiterung (sowas wie "alt" oder "backup").
     * @param numberOfDigitsInBackupFileNumber
     *            Anzahl der Stellen für die Nummer dahinter, auf diese Anzahl von Stellen wird die
     *            Nummer mit führenden Nullen aufgefüllt, damit im Explorer / Total Commander die
     *            Dateien bei Sortierung nach Name in der richtigen Reihenfolge stehen.
     * @return Den vergebenen Backup-Namen oder den leeren String, falls die Datei nicht
     *         existierte.
     */
    public static String copyFileToBackupIfExisting(String filename,
            String insertTextBeforeExtension, int numberOfDigitsInBackupFileNumber) {
        boolean move = false;
        boolean useSubdirectory = false;
        return copyOrMoveFileToBackupIfExisting(filename, insertTextBeforeExtension,
                numberOfDigitsInBackupFileNumber, move, useSubdirectory);
    }// TODO TESTEN

    /**
     * Verschiebt eine Datei auf einen Backupnamen.
     *
     * Wenn es diese schon gibt, dann wird der Backupzähler erhöht und so weiter, bis die Datei
     * nicht existiert.
     *
     * @param filename
     *            Name der Datei.
     * @param insertTextBeforeExtension
     *            Einschub vor die Erweiterung (sowas wie "alt" oder "backup").
     * @param numberOfDigitsInBackupFileNumber
     *            Anzahl der Stellen für die Nummer dahinter, auf diese Anzahl von Stellen wird die
     *            Nummer mit führenden Nullen aufgefüllt, damit im Explorer / Total Commander die
     *            Dateien bei Sortierung nach Name in der richtigen Reihenfolge stehen.
     * @return Den vergebenen Backup-Namen oder den leeren String, falls die Datei nicht
     *         existierte.
     */
    public static String moveFileToBackupIfExisting(String filename,
            String insertTextBeforeExtension, int numberOfDigitsInBackupFileNumber) {
        boolean move = true;
        boolean useSubdirectory = false;
        return copyOrMoveFileToBackupIfExisting(filename, insertTextBeforeExtension,
                numberOfDigitsInBackupFileNumber, move, useSubdirectory);
    }// TODO TESTEN

    /**
     * Verschiebt eine Datei auf einen Backupnamen in ein Unterverzeichnis.
     *
     * Wenn es diese schon gibt, dann wird der Backupzähler erhöht und so weiter, bis die Datei
     * nicht existiert.
     *
     * @param filename
     *            Name der Datei.
     * @param insertTextBeforeExtension
     *            Einschub vor die Erweiterung (sowas wie "alt" oder "backup"), dieser wird auch
     *            als Name des Unterverzeichnisses verwendet.
     * @param numberOfDigitsInBackupFileNumber
     *            Anzahl der Stellen für die Nummer dahinter, auf diese Anzahl von Stellen wird die
     *            Nummer mit führenden Nullen aufgefüllt, damit im Explorer / Total Commander die
     *            Dateien bei Sortierung nach Name in der richtigen Reihenfolge stehen.
     * @return Den vergebenen Backup-Namen oder den leeren String, falls die Datei nicht
     *         existierte.
     */
    public static String moveFileToBackupInSubdirectoryIfExisting(String filename,
            String insertTextBeforeExtension, int numberOfDigitsInBackupFileNumber) {
        boolean move = true;
        boolean useSubdirectory = true;
        return copyOrMoveFileToBackupIfExisting(filename, insertTextBeforeExtension,
                numberOfDigitsInBackupFileNumber, move, useSubdirectory);
    }// TODO TESTEN

    private static String copyOrMoveFileToBackupIfExisting(String filename,
            String insertTextBeforeExtension, int numberOfDigitsInBackupFileNumber,
            boolean move, boolean useSubdirectory) {
        String backUpFileName = "";

        if (exists(filename)) {
            int backupNumber = 0;
            boolean goOn = true;
            while (goOn) {
                ++backupNumber;
                backUpFileName = insertBeforeExtension(filename,
                        "_" + insertTextBeforeExtension + "_"
                                + NumberString.addLeadingZeroes(backupNumber,
                                        numberOfDigitsInBackupFileNumber));
                if (useSubdirectory) {
                    String dir = getDirName(backUpFileName);
                    String barename = getBareName(backUpFileName);
                    String subdir = concatPathesMultiple(dir, insertTextBeforeExtension);
                    createDirectoryIfNotExists(subdir);
                    backUpFileName = concatPathesMultiple(subdir, barename);
                }
                if (!exists(backUpFileName)) {
                    goOn = false;
                    if (move) {
                        moveFile(filename, backUpFileName);
                    }
                    else {
                        copyFile(filename, backUpFileName);
                    }
                }
            }
        }

        return backUpFileName;
    }

    /**
     * Liest die Begriffe aus einer externen Textdatei ein.
     *
     * Die Zeilen werden getrimmt. Dabei werden leere Zeilen und solche, die mit dem
     * Kommentarzeichen(ggf., nach Leerraum) überlesen. Kommt in einer Datenzeile weiter hinten das
     * Kommentarzeichen vor, so wird ab dort abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @param charset
     *            Kodierung der einzulesenden Datei.
     * @return Liste mit den Begriffen.
     */
    public static List<String> readStringsFromExternalTextFile(String filename, Charset charset) {
        StringsFromNormalTextFileReader reader = new StringsFromNormalTextFileReader(filename,
                charset, "#");
        reader.beQuiet();
        return reader.read();
    }// TODO TESTEN

    /**
     * Liest die Begriffe aus einer externen Textdatei ein und sortiert sie absteigend nach der
     * Länge.
     *
     * Die Zeilen werden getrimmt. Dabei werden leere Zeilen und solche, die mit dem
     * Kommentarzeichen(ggf., nach Leerraum) überlesen. Kommt in einer Datenzeile weiter hinten das
     * Kommentarzeichen vor, so wird ab dort abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @param charset
     *            Kodierung der einzulesenden Datei.
     * @return Liste mit den Begriffen.
     */
    public static List<String> readFromExternalFileSortByLengthDescanding(String filename,
            Charset charset) {
        List<String> lines = FileHelper.readStringsFromExternalTextFile(filename, charset);
        CollectionsHelper.sortStringListByLengthDescanding(lines);
        return lines;
    }

    /**
     * Liest die Begriffe aus einer Textdatei, die neben dem Quellcode im Jar liegt, ein.
     *
     * Die Zeilen werden getrimmt. Dabei werden leere Zeilen und solche, die mit dem
     * Kommentarzeichen(ggf., nach Leerraum) überlesen. Kommt in einer Datenzeile weiter hinten das
     * Kommentarzeichen vor, so wird ab dort abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @return Liste mit den Begriffen.
     */
    public static List<String> readStringsFromTextFileInJar(String filename) {
        return readStringsFromTextFileInJar(filename, 1);
    }// TODO TESTEN

    /**
     * Liest die Begriffe aus einer Textdatei, die neben dem Quellcode im Jar liegt, ein.
     *
     * Die Zeilen werden getrimmt. Dabei werden leere Zeilen und solche, die mit dem
     * Kommentarzeichen(ggf., nach Leerraum) überlesen. Kommt in einer Datenzeile weiter hinten das
     * Kommentarzeichen vor, so wird ab dort abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @param addToIndex
     *            Erhöhung des Index zum Ermitteln der richtigen Aufrufklasse.
     * @return Liste mit den Begriffen.
     */
    public static List<String> readStringsFromTextFileInJar(String filename, int addToIndex) {
        StringsFromTextFileReader reader = new StringsFromTextFileInJarReader(filename,
                CallerDeterminer.determineCallingClass(addToIndex));
        reader.beQuiet();
        return reader.read();
    }// TODO TESTEN

    /**
     * Liest die Begriffe aus einer Textdatei, die neben dem Quellcode im Jar liegt, ein
     * und sortiert sie dann absteigend nach der Länge.
     *
     * Die Zeilen werden getrimmt. Dabei werden leere Zeilen und solche, die mit dem
     * Kommentarzeichen(ggf., nach Leerraum) überlesen. Kommt in einer Datenzeile weiter hinten das
     * Kommentarzeichen vor, so wird ab dort abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @return Liste mit den Begriffen.
     */
    public static List<String> readStringsFromTextFileInJarAndSortByLengthDescanding(
            String filename) {
        List<String> list = readStringsFromTextFileInJar(filename, 1);

        CollectionsHelper.sortStringListByLengthDescanding(list);
        return list;
    }// TODO TESTEN

    /**
     * Liest eine Liste mit den Texten im Format des DictionaryFiles aus den Zeilen
     * einer Textdatei ein.
     *
     * Diese Klasse liest eine Liste mit den Texten im Format des DictionaryFiles aus den Zeilen
     * einer Textdatei ein.
     *
     * Das Format sieht wie folgt aus:
     *
     * Englisches Wort
     *     Deutsches Wort 1
     *     Deutsches Wort 2
     *     Deutsches Wort 3
     *     Deutsches Wort 4
     *
     * Kommt in einer Datenzeile weiter hinten das Kommentarzeichen vor, so wird ab dort
     * abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @return Liste von Listen aus der Datei. Das erste Wort in der Liste ist die Vokabel und die
     *         anderen die Übersetzungen.
     */
    public static Dictionary readDictionaryTextFileInJar(String filename) {
        return readDictionaryTextFileInJar(filename, 1);
    }

    /**
     * Liest eine Liste mit den Texten im Format des DictionaryFiles aus den Zeilen
     * einer Textdatei ein.
     *
     * Das Format sieht wie folgt aus:
     *
     * Englisches Wort
     *     Deutsches Wort 1
     *     Deutsches Wort 2
     *     Deutsches Wort 3
     *     Deutsches Wort 4
     *
     * Kommt in einer Datenzeile weiter hinten das Kommentarzeichen vor, so wird ab dort
     * abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der Textdatei ohne Pfad.
     * @param addToIndex
     *            Erhöhung des Index zum Ermitteln der richtigen Aufrufklasse.
     * @return Liste von Listen aus der Datei. Das erste Wort in der Liste ist die Vokabel und die
     *         anderen die Übersetzungen.
     */
    public static Dictionary readDictionaryTextFileInJar(String filename, int addToIndex) {
        DictionaryFromTextFileReader reader = new DictionaryFromTextFileInJarReader(filename,
                CallerDeterminer.determineCallingClass(addToIndex));
        reader.beQuiet();
        reader.read();
        return reader.getDictionary();
    }

    /**
     * Liest eine Liste mit den Texten im Format des DictionaryFiles aus den Zeilen
     * einer Textdatei im UTF-8-Format ein.
     *
     * Das Format sieht wie folgt aus:
     *
     * Englisches Wort
     *     Deutsches Wort 1
     *     Deutsches Wort 2
     *     Deutsches Wort 3
     *     Deutsches Wort 4
     *
     * Kommt in einer Datenzeile weiter hinten das Kommentarzeichen vor, so wird ab dort
     * abgeschnitten und getrimmt.
     *
     * @param filename
     *            Name der einzulesenden Datei.
     * @param charset
     *            Kodierung der einzulesenden Datei.
     */
    public static Dictionary readDictionaryFromNormalTextFile(String filename,
            Charset charset) {
        DictionaryFromTextFileReader reader = new DictionaryFromNormalTextFileReader(filename,
                charset);
        reader.beQuiet();
        reader.read();
        return reader.getDictionary();
    }

    /**
     * Liest eine Liste mit den Texten im Format des DictionaryFiles aus den Zeilen
     * einer Textdatei im UTF-8-Format ein.
     *
     * Das Format sieht wie folgt aus:
     *
     * Englisches Wort
     *     Deutsches Wort 1
     *     Deutsches Wort 2
     *     Deutsches Wort 3
     *     Deutsches Wort 4
     *
     * Kommt in einer Datenzeile weiter hinten das Kommentarzeichen vor, so wird ab dort
     * abgeschnitten und getrimmt.
     *
     * Mehrfach vorkommende Einträge in einer Liste des Wörterbuchs werden als Fehler gewertet.
     *
     * @param filename
     *            Name der einzulesenden Datei.
     * @param charset
     *            Kodierung der einzulesenden Datei.
     */
    public static Dictionary readDictionaryFromNormalTextFileMultipleEqualValuesAreErrors(
            String filename, Charset charset) {
        DictionaryFromTextFileReader reader = new DictionaryFromNormalTextFileReader(filename,
                charset);
        reader.beQuiet();
        reader.multipleEqualValuesAreError();
        reader.read();
        return reader.getDictionary();
    }

    /**
     * Ersetzt in einer Eingabe, die Teil eines Dateinamens werden soll, alle in Dateinamen nicht
     * erlaubten Zeichen durch das angegebene.
     *
     * Vgl. https://stackoverflow.com/questions/1976007/
     *       what-characters-are-forbidden-in-windows-and-linux-directory-names
     *
     * @param filenamePart
     *            Teil eines Dateinamens in dem verbotene Zeichen ersetzt werden sollen.
     * @param replacement
     *            Die verbotenen Zeichen werden hierdurch ersetzt.
     */
    public static String replaceForbiddenFilenameCharacters(String filenamePart,
            String replacement) {

        String replaced = filenamePart;

        for (String forbiddenFilenameCharacter : FORBIDDEN_FILENAME_CHARACTERS) {
            replaced = replaced.replace(forbiddenFilenameCharacter, replacement);
        }

        return replaced;
    }

    /**
     * Liest eine Textdatei aus einer Jar im gleichen Paket (Verzeichnis) wie die übergebene Klasse
     * ein.
     *
     * @param clazz
     *            Klasse neben der die Textdatei liegt.
     * @param bareFilename
     *            Name der Textdatei ohne Pfad.
     * @return Eingelesener Text.
     */
    public static String readFileInJarBesideJavaClass(Class<?> clazz, String bareFilename) {
        List<String> list = readFileInJarBesideJavaClassAsList(clazz, bareFilename);
        return Text.join("\n", list);
        /*
         * Hier wäre Text.LINE_BREAK sehr viel geeigneter, aber da die Methode schon an so vielen
         * Stellen verwendet wird, weiß ich nicht, was das dann kaputt macht.
         */

        /*
        try {
            return tryToRreadFileInJarBesideJavaClass(clazz, bareFilename);
        }
        catch (Exception exception) {
            throw new RuntimeException(
                    "Fehler beim Lesen der Datei '" + bareFilename + "' aus dem Jar.", exception);
        }
        */
    }

    /*
    private static String tryToRreadFileInJarBesideJavaClass(Class<?> clazz, String bareFilename)
            throws IOException {
        InputStream is = clazz.getResourceAsStream(bareFilename);
        InputStreamReader reader = new InputStreamReader(is, "UTF-8");
        BufferedReader in = new BufferedReader(reader);
        String line;
        StringBuilder builder = new StringBuilder();
        while (null != (line = in.readLine())) {
            builder.append(line);
            builder.append("\n");
        }
        in.close();
        return builder.toString();
    }
    */

    /**
     * Liest eine Textdatei aus einer Jar im gleichen Paket (Verzeichnis) wie die übergebene Klasse
     * ein.
     *
     * @param clazz
     *            Klasse neben der die Textdatei liegt.
     * @param bareFilename
     *            Name der Textdatei ohne Pfad.
     * @return Eingelesener Text als Liste von Zeilen.
     */
    public static List<String> readFileInJarBesideJavaClassAsList(Class<?> clazz,
            String bareFilename) {
        try {
            return tryToRreadFileInJarBesideJavaClassAsList(clazz, bareFilename);
        }
        catch (Exception exception) {
            throw new RuntimeException(
                    "Fehler beim Lesen der Datei '" + bareFilename + "' aus dem Jar.", exception);
        }
    }

    private static List<String> tryToRreadFileInJarBesideJavaClassAsList(Class<?> clazz,
            String bareFilename) throws IOException {
        InputStream is = clazz.getResourceAsStream(bareFilename);
        InputStreamReader reader = new InputStreamReader(is, "UTF-8");
        BufferedReader in = new BufferedReader(reader);
        String line;
        List<String> lines = new ArrayList<>();
        while (null != (line = in.readLine())) {
            lines.add(line);
        }
        in.close();
        return lines;
    }

    /**
     * Liest eine Textdatei aus einer Jar im gleichen Paket (Verzeichnis) wie die übergebene Klasse
     * ein.
     *
     * @param clazz
     *            Klasse neben der die Textdatei liegt.
     * @param bareFilename
     *            Name der Textdatei ohne Pfad.
     * @return Eingelesener Text als Liste von Zeilen.
     */
    public static Lines readFileInJarBesideJavaClassAsLines(Class<?> clazz,
            String bareFilename) {
        List<String> list = readFileInJarBesideJavaClassAsList(clazz, bareFilename);
        return new Lines(list);
    }

    /**
     * Schreibt die Titel- und Datenfelder in die Datei mit dem angegebenen Namen.
     *
     * @param titles
     *            Die Liste mit den Felder der Titelzeile
     * @param valuesList
     *            Die Liste mit den Listen Feldern der einzelnen Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     */
    public static void writeTitlesAndValues(List<String> titles, List<List<String>> valuesList,
            String filename) {
        writeTitlesAndValues(new FineFileWriter(filename), titles, valuesList);
    }

    /**
     * Schreibt die Titel- und Datenfelder unter Verwendung des angegebenen Encodings in die Datei
     * mit dem angegebenen Namen.
     *
     * @param titles
     *            Die Liste mit den Felder der Titelzeile
     * @param valuesList
     *            Die Liste mit den Listen Feldern der einzelnen Zeilen.
     * @param filename
     *            Name der zu erzeugenden Datei.
     * @param charset
     *            Encoding der Datei.
     */
    public static void writeTitlesAndValues(List<String> titles, List<List<String>> valuesList,
            String filename, Charset charset) {
        writeTitlesAndValues(new FineFileWriter(filename, charset), titles, valuesList);
    }

    private static void writeTitlesAndValues(FineFileWriter writer, List<String> titles,
            List<List<String>> valuesList) {
        if (!titles.isEmpty()) {
            writer.writeFieldsLine(titles);
        }
        for (List<String> values : valuesList) {
            writer.writeFieldsLine(values);
        }
        writer.close();
    }

    /**
     * Schreibt eine Datei mit Daten, die Titel und Werte haben.
     *
     * Wenn die Liste leer ist, wird eine leere Datei (auch ohne Titelzeile) erzeugt.
     *
     * @param filename
     *            Name der Datei.
     * @param datasets
     *            Liste mit den Datenobjekten.
     */
    public static void writeDataWithTitlesAndValuesToFile(String filename,
            List<? extends DataWithTitlesAndValues> datasets) {
        List<String> titles = new ArrayList<>();
        List<List<String>> valuesList = new ArrayList<>();
        if (!datasets.isEmpty()) {
            DataWithTitlesAndValues firstEntry = datasets.get(0);
            titles.addAll(firstEntry.titles());
        }
        for (DataWithTitlesAndValues entry : datasets) {
            valuesList.add(entry.values());
        }
        FileHelper.writeTitlesAndValues(titles, valuesList, filename);
    }

    /**
     * Schreibt eine Datei unter Verwendung des angegebenen Encodings mit Daten, die Titel und
     * Werte haben.
     *
     * Wenn die Liste leer ist, wird eine leere Datei (auch ohne Titelzeile) erzeugt.
     *
     * @param filename
     *            Name der Datei.
     * @param charset
     *            Encoding der Datei.
     * @param datasets
     *            Liste mit den Datenobjekten.
     */
    public static void writeDataWithTitlesAndValuesToFile(String filename, Charset charset,
            List<? extends DataWithTitlesAndValues> datasets) {
        List<String> titles = new ArrayList<>();
        List<List<String>> valuesList = new ArrayList<>();
        if (!datasets.isEmpty()) {
            DataWithTitlesAndValues firstEntry = datasets.get(0);
            titles.addAll(firstEntry.titles());
        }
        for (DataWithTitlesAndValues entry : datasets) {
            valuesList.add(entry.values());
        }
        FileHelper.writeTitlesAndValues(titles, valuesList, filename, charset);
    }

    /**
     * Schreibt eine Datei mit Daten, die Titel und Werte haben.
     *
     * Wenn die Liste leer ist, wird eine leere Datei mit den Titeln für die Leere Datei erzeugt.
     *
     * @param filename
     *            Name der Datei.
     * @param datasets
     *            Liste mit den Datenobjekten.
     * @param titlesForEmptyFile
     *            Titel für eine leere Datei.
     */
    public static void writeDataWithTitlesAndValuesToFile(String filename,
            List<? extends DataWithTitlesAndValues> datasets, List<String> titlesForEmptyFile) {
        List<String> titles = new ArrayList<>();
        List<List<String>> valuesList = new ArrayList<>();
        if (datasets.isEmpty()) {
            titles.addAll(titlesForEmptyFile);
        }
        else {
            DataWithTitlesAndValues firstEntry = datasets.get(0);
            titles.addAll(firstEntry.titles());
        }
        for (DataWithTitlesAndValues entry : datasets) {
            valuesList.add(entry.values());
        }
        FileHelper.writeTitlesAndValues(titles, valuesList, filename);
    }

    /**
     * Schreibt eine Datei mit Daten, die Titel und Werte haben.
     *
     * Wenn die Liste leer ist, wird eine leere Datei (auch ohne Titelzeile) erzeugt.
     *
     * @param barefilename
     *            Name der Datei ohne Pfad.
     * @param directory
     *            Verzeichnis in dem die Datei erstellt werden soll.
     * @param datasets
     *            Liste mit den Datenobjekten.
     */
    public static void writeDataWithTitlesAndValuesToFile(String barefilename, String directory,
            List<? extends DataWithTitlesAndValues> datasets) {
        String filename = FileHelper.concatPathes(directory, barefilename);
        FileHelper.writeDataWithTitlesAndValuesToFile(filename, datasets);
    }

    /** Erzeugt eine einzelne Backupdatei indem '_backup' vor die Extension eingeschoben wird. */
    public static void createOneBackupFile(String filename) {
        String backupFilename = FileHelper.insertBeforeExtension(filename, "_backup");
        if (FileHelper.exists(filename)) {
            FileHelper.copyFile(filename, backupFilename);
        }
    }

    /** Prüft ob der übergebene Dateinamen einer ohne Pfad ist. */
    public static boolean isBareFilename(String filename) {
        if (filename.contains("/")) {
            return false;
        }
        if (filename.contains("\\")) {
            return false;
        }
        return true;
    }

    /**
     * Gibt aus einer Liste von Dateinamen mit Pfaden diejenigen zurück, deren Dateiname ohne Pfad der
     * übergebenen Dateinamen ohne Pfad entsprechen.
     *
     * @param filenames
     *            Die Liste der zu durchsuchenden Dateinamen mit Pfad.
     * @param bareFilename
     *            Der gewünschte Dateinamen ohne Pfad.
     * @return Liste der Dateinamen mit Pfad, welche ohne Pfad dem gegebenen Dateinamen ohne Pfad
     *         gleichen.
     */
    public static List<String> getFilesWithGivenBareFilename(List<String> filenames,
            String bareFilename) {
        if (!isBareFilename(bareFilename)) {
            throw new IllegalArgumentException(
                    "Der übergebene Dateiname ohne Pfad enthält einen Pfad: " + bareFilename);
        }

        List<String> list = new ArrayList<>();

        for (String filename : filenames) {
            if (isBareFilename(filename)) {
                throw new IllegalArgumentException(
                        "Der Dateiname in der liste enthält keinen Pfad: " + filename);
            }
            String bareFilenameOfFile = FileHelper.getBareName(filename);
            if (bareFilenameOfFile.equals(bareFilename)) {
                list.add(filename);
            }
        }

        return list;
    }

    /**
     * Gibt aus einer Liste von Dateinamen diejenigen zurück, deren Dateiname die gewünschte
     * Extension hat.
     *
     * @param filenames
     *            Die Liste der zu durchsuchenden Dateinamen.
     * @param extension
     *            Der gewünschte Extension (sie kann mit oder ohne führenden Punkt angegeben
     *            werden).
     * @return Liste der Dateinamen, die die gewünschte Extension hat.
     */
    public static List<String> getFilesWithGivenExtension(List<String> filenames, String extension) {
        String searchEnd;
        if (extension.startsWith(".")) {
            searchEnd = extension;
        }
        else {
            searchEnd = "." + extension;
        }

        List<String> list = new ArrayList<>();

        for (String filename : filenames) {
            if (filename.endsWith(searchEnd)) {
                list.add(filename);
            }
        }

        return list;
    }

    /**
     * Ermittelt für alle Dateinamen aus der Liste den Teil des übergebenen Dateinamens nach dem
     * letzten Slash oder Backslash und gibt diese als Liste zurück.
     */
    public static List<String> filenamesToBareFilenames(List<String> filenames) {
        List<String> bareFilenames = new ArrayList<>();

        for (String filename : filenames) {
            String bareFilename = FileHelper.getBareName(filename);
            bareFilenames.add(bareFilename);
        }

        return bareFilenames;
    }

    /**
     * Erzeugt aus eine Liste mit Dateinamen eine neue Liste, die nur de Dateinamen beinhaltet, die
     * noch im Dateisystem zu finden sind.
     */
    public static List<String> createExistingFileList(List<String> filenames) {
        List<String> existingFilenames = new ArrayList<>();

        for (String filename : filenames) {
            if (isFile(filename)) {
                existingFilenames.add(filename);
            }
        }

        return existingFilenames;
    }

    /**
     * Prüft, ob die übergebene Datei auf das übergebene Ende endet.
     *
     * Dabei wird der Dateiname zum Test in Kleinbuchstaben gewandelt, die Liste muss also
     * kleingeschriebene Endungen enthalten, z.B. (".mp3", ".m4a").
     *
     * @param filename
     *            Der zu überprüfende Dateiname.
     * @param endings
     *            Die Liste der möglichen Endungen.
     * @return Wahr genau dann, wenn der Dateiname auf eine der Endungen endet.
     */
    public static boolean endsWithExtension(String filename, String ending) {
        return endsWithOneOfMultipleExtensions(filename, CollectionsHelper.buildListFrom(ending));
    }

    /**
     * Prüft, ob die übergebene Datei auf eine der übergebenden Enden endet.
     *
     * Dabei wird der Dateiname zum Test in Kleinbuchstaben gewandelt, die Liste muss also
     * kleingeschriebene Endungen enthalten, z.B. (".mp3", ".m4a").
     *
     * @param filename
     *            Der zu überprüfende Dateiname.
     * @param endings
     *            Die Liste der möglichen Endungen.
     * @return Wahr genau dann, wenn der Dateiname auf eine der Endungen endet.
     */
    public static boolean endsWithOneOfMultipleExtensions(String filename, List<String> endings) {
        String loweredFilename = Text.toLowerCase(filename);

        for (String extension : endings) {
            if (loweredFilename.endsWith(extension))
                return true;
        }

        return false;
    }

    /**
     * Gibt an, ob das übergebene Kommando mit einer im Dateisystem vorhandenen Datei beginnt.
     * Getrennt wird es dazu am ersten Leerzeichen, enthält der Pfad solche, funktioniert es so
     * nicht. Dann muss man von Hand nach einer Endung wie '.exe' etc. suchen.
     */
    public static boolean commandStartsWithExistingFilename(String command) {
        if (command.isBlank()) {
            return false;
        }
        int firstSpaceIndex = command.indexOf(" ");
        if (firstSpaceIndex < 1) {
            return false;
        }

        String filename = command.substring(0, firstSpaceIndex);
        return isFile(filename);
    }

    /**
     * Gibt den Dateinamen der einen Datei ohne Pfad zurück, welche den Kriterien entspricht. Gibt
     * es keine oder mehr als eine solche Datei, wird eine Ausnahme geworfen.
     *
     * @param directory
     *            Das Verzeichnis in dem die Datei gesucht wird.
     * @param bareNameStart
     *            Der Anfang des Dateinamens der gesuchten Datei ohne Pfad.
     * @param bareNameEnd
     *            Das Ende des Dateinamens der gesuchten Datei ohne Pfad.
     */
    public static String findTheOneFileInMainDirectory(String directory, String bareNameStart,
            String bareNameEnd) {
        List<String> filenames = findAllFilesInMainDirectoryNio2(directory);

        List<String> foundBareFilenames = new ArrayList<>();
        for (String filename : filenames) {
            String bareFilename = getBareName(filename);
            if (bareFilename.startsWith(bareNameStart) && bareFilename.endsWith(bareNameEnd)) {
                foundBareFilenames.add(bareFilename);
            }
        }

        if (foundBareFilenames.size() == 1) {
            return foundBareFilenames.get(0);
        }
        else {
            throw new RuntimeException("Es wurde nicht genau eine Datei mit den gewünschten "
                    + "Kriterien gefunden:\n"
                    + "\t" + "directory                 = " + directory + "\n"
                    + "\t" + "bareNameStart             = " + bareNameStart + "\n"
                    + "\t" + "bareNameEnd               = " + bareNameEnd + "\n"
                    + "\t" + "Anzahl gefundener Dateien : " + foundBareFilenames.size() + "\n"
                    + CollectionsHelper.listListNice(foundBareFilenames)
                    );
        }
    }

    /** Wirft eine Ausnahme, wenn das übergebene Verzeichnis nicht existiert. */
    public static void checkDirectoryExistence(String directory) {
        if (!isDirectory(directory)) {
            throw new RuntimeException("Das Verzeichnis '" + directory + "' existiert nicht.");
        }
    }

}
