public class InfinitiveLoopException
        extends Exception
        implements Serializable {

    /** Serial version UID. */
    private static final long serialVersionUID = 4052546955101609848L;

    /**
     * Constructs an instance of <code>InfinitiveLoopException</code> with the specified detail message.
     *
     * @param msg the detail message.
     */
    public InfinitiveLoopException(String msg) {
        super(msg);
    }
} public class StringHelper {

    /** The white-space character (<code>'&#92;u0020'</code>). */
    public static final char SPACE = '\u0020';

    /** The tabulation character (<code>'&#92;u0009'</code>). */
    public static final char TAB = '\u0009';

    /** The Carriage Return character (CR). */
    public static final char CARRIAGE_RETURN = '\r';

    /** The Line Feed character (LF). */
    public static final char LINE_FEED = '\n';

    /** Hexadecimal digits. */
    private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    /** Suppresses default constructor, ensuring non-instantiability. */
    private StringHelper() {
    }

    /**
     * Check a regular expression.
     *
     * @param src the text to examine.
     * @param pattern the compiled regular expression, targeted area are enclosed with parenthesis.
     * @return <code>true</code> if the regular expression is checked, otherwise <code>false</code>.
     */
    public static boolean checkRegex(String src, Pattern pattern) {
        return pattern.matcher(src).find();
    }

    /**
     * Return <em>N</em> concatenations of string.
     * <p>Example: <code>StringHelper.generateStr("AB", 4)</code> will return "ABABABAB".
     *
     * @param model the string to concatenate.
     * @param occurences the number of concatenations. If zero of negative, the method will return an empty string.
     * @return the concatenations result.
     */
    public static String generateStrConcat(String model, int occurences) {
        String resConcat;
        if (occurences > 0) {
            StringBuilder buffer = new StringBuilder(model.length() * occurences);
            for (int numOcc = 0; numOcc < occurences; numOcc++) {
                buffer.append(model);
            }
            resConcat = buffer.toString();
        } else {
            resConcat = "";
        }
        return resConcat;
    }

    /**
     * Return the capturing groups from the regular expression in the string.
     *
     * @param src the string to search in.
     * @param pattern the compiled pattern.
     * @param expectedNbMatchs the expected tokens matched, for performance purpose.
     * @return all the strings matched (in an ArrayList).
     */
    public static List<String> getGroupsFromRegex(String src, Pattern pattern, int expectedNbMatchs) {
        List<String> res = new ArrayList<>(expectedNbMatchs);
        Matcher matcher = pattern.matcher(src);
        while (matcher.find()) {
            for (int group = 1; group <= matcher.groupCount(); group++) {
                res.add(matcher.group(group));
            }
        }
        return res;
    }

    /**
     * Find the "prefix" string the most at the left in the source string.<br />
     * Example: with "my name is" as the source string, and {"name", "is"} as the prefix strings, "name" is returned.
     *
     * @param str the source string.
     * @param prefixes the prefix strings array.
     * @return the prefix that is the most at the left. If no prefix is found, <code>null</code> is returned.
     */
    public static String mostLeft(String str, String[] prefixes) {
        String prefixFound = null;
        int bestPrefixPos = str.length();
        for (String pre : prefixes) {
            int pos = str.indexOf(pre);
            if (pos != -1 && pos < bestPrefixPos) {
                bestPrefixPos = pos;
                prefixFound = pre;
            }
        }
        return prefixFound;
    }

    /**
     * Find the "suffix" string the most at the right in the source string.<br />
     * Example: with "my name is" as the source string, and {"name", "is"} as the suffix strings array, "is" is returned.
     *
     * @param str the source string.
     * @param suffixes the suffix strings array.
     * @return the suffix that is the most at the right. If no suffix is found, <code>null</code> is returned.
     */
    public static String mostRight(String str, String[] suffixes) {
        String suffixFound = null;
        int bestSuffixPos = -1;
        for (String pre : suffixes) {
            int pos = str.lastIndexOf(pre);
            if (pos != -1 && pos > bestSuffixPos) {
                bestSuffixPos = pos;
                suffixFound = pre;
            }
        }
        return suffixFound;
    }

    /**
     * Replace the first substring of this string that matches the given compiled pattern with the literalized given replacement.
     *
     * @param src the character sequence to be matched.
     * @param pattern the compiled regular expression to which this string is to be matched.
     * @param replacement the replacement string (internally literalized).
     * @return the resulting string.
     */
    public static String replaceFirst(String src, Pattern pattern, String replacement) {
        return pattern.matcher(src).replaceFirst(Matcher.quoteReplacement(replacement));
    }

    /**
     * Splits this string around matches of the given regular expression. All computed results have their leading and trailing white-space
     * characters (<code>'&#92;u0020'</code>) and tabulations (<code>'&#92;u0009'</code>) removed.
     *
     * @param str the string to split.
     * @param regex the delimiting regular expression.
     * @return the array of strings computed by splitting this string around matches of the given regular expression.
     */
    public static String[] tSplit(String str, String regex) {
        String[] res = str.split(Matcher.quoteReplacement(regex), -1);
        for (int nElt = 0; nElt < res.length; nElt++) {
            res[nElt] = tTrim(res[nElt]);
        }
        return res;
    }

    /**
     * Indicate if all the strings are null or length equals to zero or are entirely packed with spaces and/or tabulations.
     *
     * @param strs the strings to check.
     * @return <code>true</code> if all the strings are empty, otherwise <code>false</code>.
     */
    public static boolean strAreEmpty(String... strs) {
        boolean res = true;
        for (String s : strs) {
            if (strIsNotEmpty(s)) {
                res = false;
                break;
            }
        }
        return res;
    }

    /**
     * Indicate if the string is null or length equals to zero or is entirely packed with spaces and/or tabulations.
     *
     * @param str the string to check.
     * @return <code>true</code> if the string is empty, otherwise <code>false</code>.
     */
    public static boolean strIsEmpty(String str) {
        return (null == str) || (0 == tTrim(str).length());
    }

    /**
     * Indicate if the string is not null and length greater than zero and is not entirely packed with spaces and/or tabulations.
     *
     * @param str the string to check.
     * @return <code>true</code> if the string is not empty, otherwise <code>false</code>.
     */
    public static boolean strIsNotEmpty(String str) {
        return (null != str) && (0 < tTrim(str).length());
    }

    /**
     * Indicate if the string starts with one of the specified prefixes.
     *
     * @param str the source string.
     * @param prefixes the list of prefixes.
     * @return <code>true</code> if the source string starts with one of these prefixes, otherwise <code>false</code>.
     */
    public static boolean strStartsWith(String str, String... prefixes) {
        boolean res = false;
        for (String s : prefixes) {
            if (str.startsWith(s)) {
                res = true;
                break;
            }
        }
        return res;
    }

    /**
     * Indicate if the string ends with one of the specified prefixes.
     *
     * @param str the source string.
     * @param suffixes the list of prefixes.
     * @return <code>true</code> if the source string ends with one of these prefixes, otherwise <code>false</code>.
     */
    public static boolean strEndsWith(String str, String... suffixes) {
        boolean res = false;
        for (String s : suffixes) {
            if (str.endsWith(s)) {
                res = true;
                break;
            }
        }
        return res;
    }

    /**
     * Return a copy of the string, with leading and trailing {@link #SPACE} and {@link #TAB} omitted.
     *
     * @param str the string.
     * @return A copy of this string with leading and trailing white-space/tab removed, or this string if it has no leading or trailing
     *         white-space/tab.
     */
    public static String tTrim(String str) {
        return tTrim(str, false);
    }

    /**
     * Return a copy of the string, with leading and trailing {@link #SPACE} and {@link #TAB} omitted.
     *
     * @param str the string.
     * @param removeCRLF remove leading and trailing ({@link #CARRIAGE_RETURN} and {@link #LINE_FEED}).
     * @return A copy of this string with leading and trailing white-space/tab removed, or this string if it has no leading or trailing
     *         white-space/tab.
     */
    public static String tTrim(String str, boolean removeCRLF) {
        int strlen = str.length();
        int endpos = strlen;
        int startpos = 0;
        char[] val = new char[strlen];
        str.getChars(0, strlen, val, 0);
        if (removeCRLF) {
            while ((startpos < strlen) && ((val[startpos] == SPACE) || (val[startpos] == TAB)
                    || (val[startpos] == CARRIAGE_RETURN) || (val[startpos] == LINE_FEED))) {
                startpos++;
            }
            while ((endpos > startpos) && ((val[endpos - 1] == SPACE) || (val[endpos - 1] == TAB)
                    || (val[startpos - 1] == CARRIAGE_RETURN) || (val[startpos - 1] == LINE_FEED))) {
                endpos--;
            }
        } else {
            while ((startpos < strlen) && ((val[startpos] == SPACE) || (val[startpos] == TAB))) {
                startpos++;
            }
            while ((endpos > startpos) && ((val[endpos - 1] == SPACE) || (val[endpos - 1] == TAB))) {
                endpos--;
            }
        }
        return ((startpos > 0) || (endpos < strlen)) ? str.substring(startpos, endpos) : str;
    }

    /**
     * Return a copy of the string, with leading (on the left side) {@link #SPACE}, {@link #TAB} omitted.
     *
     * @param str the string.
     * @return A copy of this string with leading white-space/tab removed, or this string if it has no leading white-space/tab.
     */
    public static String tTrimLead(String str) {
        return tTrimLead(str, false);
    }

    /**
     * Return a copy of the string, with leading (on the left side) {@link #SPACE}, {@link #TAB} omitted, and optionally
     * {@link #CARRIAGE_RETURN} and {@link #LINE_FEED} too.
     *
     * @param str the string.
     * @param removeCRLF remove leading ({@link #CARRIAGE_RETURN} and {@link #LINE_FEED}).
     * @return A copy of this string with leading white-space/tab removed, or this string if it has no leading white-space/tab.
     */
    public static String tTrimLead(String str, boolean removeCRLF) {
        int strlen = str.length();
        int startpos = 0;
        char[] val = new char[strlen];
        str.getChars(0, strlen, val, 0);
        if (removeCRLF) {
            while ((startpos < strlen) && ((val[startpos] == SPACE) || (val[startpos] == TAB)
                    || (val[startpos] == CARRIAGE_RETURN) || (val[startpos] == LINE_FEED))) {
                startpos++;
            }
        } else {
            while ((startpos < strlen) && ((val[startpos] == SPACE) || (val[startpos] == TAB))) {
                startpos++;
            }
        }
        return (startpos > 0) ? str.substring(startpos, strlen) : str;
    }

    /**
     * Return a copy of the string, with trailing (on the right side) {@link #SPACE}, {@link #TAB} omitted.
     *
     * @param str the string.
     * @return A copy of this string with trailing white-space/tab removed, or this string if it has no trailing white-space/tab.
     */
    public static String tTrimTrail(String str) {
        return tTrimTrail(str, false);
    }

    /**
     * Return a copy of the string, with trailing (on the right side) {@link #SPACE}, {@link #TAB} omitted, and optionally
     * {@link #CARRIAGE_RETURN} and {@link #LINE_FEED} too.
     *
     * @param str the string.
     * @param removeCRLF remove trailing New Lines ({@link #CARRIAGE_RETURN} and {@link #LINE_FEED}).
     * @return A copy of this string with trailing white-space/tab removed, or this string if it has no trailing white-space/tab.
     */
    public static String tTrimTrail(String str, boolean removeCRLF) {
        int strlen = str.length();
        int endpos = strlen;
        char[] val = new char[strlen];
        str.getChars(0, strlen, val, 0);
        if (removeCRLF) {
            while ((endpos > 0) && ((val[endpos - 1] == SPACE) || (val[endpos - 1] == TAB)
                    || (val[endpos - 1] == CARRIAGE_RETURN) || (val[endpos - 1] == LINE_FEED))) {
                endpos--;
            }
        } else {
            while ((endpos > 0) && ((val[endpos - 1] == SPACE) || (val[endpos - 1] == TAB))) {
                endpos--;
            }
        }
        return (endpos < strlen) ? str.substring(0, endpos) : str;
    }

    /**
     * Return a concatenation of all values of a <code>Collection</code>, with a specified separator between all of them.
     * <p>Example: with Collection of three elements "AB", "C" and "D", and a "--" separator, this method will return "AB--C--D". With an
     * empty separator, the result would be "ABCD".
     *
     * @param collection the <code>Collection</code> to decompose.
     * @param separatorBetween the separator. It can be an empty string.
     * @return the <code>Collection</code> decomposition.
     */
    public static String decomposeCollection(Collection<String> collection, String separatorBetween) {
        return decomposeCollection(collection, separatorBetween, (collection.size() * separatorBetween.length() * 16) + 32);
    }

    /**
     * Return a concatenation of all values of a <code>Collection</code>, with a specified separator between all of them.
     * <p>Example: with Collection of three elements "AB", "C" and "D", and a "--" separator, this method will return "AB--C--D". With an
     * empty separator, the result would be "ABCD".
     *
     * @param collection the <code>Collection</code> to decompose.
     * @param separatorBetween the separator. It can be an empty string.
     * @param expectedResSize the expected size of decomposition. For performance purpose only.
     * @return the <code>Collection</code> decomposition.
     */
    public static String decomposeCollection(Collection<String> collection, String separatorBetween, int expectedResSize) {
        String decomposition;
        if (collection.isEmpty()) {
            decomposition = "";
        } else {
            StringBuilder buffer = new StringBuilder(expectedResSize);
            for (String line : collection) {
                buffer.append(line).append(separatorBetween);
            }
            buffer.delete(buffer.length() - separatorBetween.length(), buffer.length());
            decomposition = buffer.toString();
        }
        return decomposition;
    }

    /**
     * States used by {@link StringHelper#convertUnicodeEscapesToUTF8(java.lang.String)}. For internal use only.
     */
    private static enum UnicodeConvertParseState {

        NORMAL,
        ESCAPE,
        UNICODE_ESCAPE

    }

    /**
     * Convert Unicode escapes to UTF-8 characters.
     * Mainly based on Dr Xi code (<a href="http://www.xinotes.org/notes/note/813/">http://www.xinotes.org/notes/note/813/</a>).
     *
     * @param unicodeString the string to convert Unicode escapes.
     * @return the String where Unicode escapes have been converted to UTF-8 characters.
     * @throws CharConversionException if an invalid Unicode escape is detected.
     */
    @SuppressWarnings("ValueOfIncrementOrDecrementUsed")
    public static String convertUnicodeEscapesToUTF8(String unicodeString)
            throws CharConversionException {
        String res;
        if (strIsEmpty(unicodeString)) { // Test added: avoid NullPointerException on empty strings.
            res = unicodeString;
        } else {
            char[] out = new char[unicodeString.length()];
            UnicodeConvertParseState state = UnicodeConvertParseState.NORMAL;
            int normalCounter = 0, uniCounter = 0, unicode = 0;
            char currChar = ' ';
            int uniStrLen = unicodeString.length();
            for (int offset = 0; offset < uniStrLen; offset++) {
                currChar = unicodeString.charAt(offset);
                if (state == UnicodeConvertParseState.ESCAPE) {
                    if (currChar == 'u') {
                        state = UnicodeConvertParseState.UNICODE_ESCAPE;
                        unicode = 0;
                    } else { // We don't care about other escapes.
                        out[normalCounter++] = '\\';
                        out[normalCounter++] = currChar;
                        state = UnicodeConvertParseState.NORMAL;
                    }
                } else if (state == UnicodeConvertParseState.UNICODE_ESCAPE) {
                    if ((currChar >= '0') && (currChar <= '9')) {
                        unicode = (unicode << 4) + currChar - '0';
                    } else if ((currChar >= 'a') && (currChar <= 'f')) {
                        unicode = (unicode << 4) + 10 + currChar - 'a';
                    } else if ((currChar >= 'A') && (currChar <= 'F')) {
                        unicode = (unicode << 4) + 10 + currChar - 'A';
                    } else {
                        throw new CharConversionException("Malformed Unicode escape");
                    }
                    uniCounter++;
                    if (uniCounter == 4) {
                        out[normalCounter++] = (char) unicode;
                        uniCounter = 0;
                        state = UnicodeConvertParseState.NORMAL;
                    }
                } else if (currChar == '\\') {
                    state = UnicodeConvertParseState.ESCAPE;
                } else {
                    out[normalCounter++] = currChar;
                }
            }
            if (state == UnicodeConvertParseState.ESCAPE) {
                out[normalCounter++] = currChar;
            }
            res = new String(out, 0, normalCounter);
        }
        return res;
    }

    /**
     * Convert UTF-8 characters to Unicode escapes.
     * Mainly based on Dr Xi code (<a href="http://www.xinotes.org/notes/note/812/">http://www.xinotes.org/notes/note/812/</a>).
     *
     * @param utf8String the string to convert UTF-8 characters.
     * @return the String where UTF-8 characters have been converted to Unicode escapes.
     */
    public static String convertUTF8ToUnicodeEscapes(String utf8String) {
        String res;
        if (strIsEmpty(utf8String)) { // Test added: avoid NullPointerException on empty strings.
            res = utf8String;
        } else {
            StringBuilder resBuffer = new StringBuilder((int) (utf8String.length() * 1.25));
            for (int offset = 0; offset < utf8String.length(); offset++) {
                char currChar = utf8String.charAt(offset);
                if ((currChar >> 7) > 0) {
                    resBuffer.append("\\u");
                    resBuffer.append(HEX_CHAR[(currChar >> 12) & 0xF]); // Append the hex character for the left-most 4-bits.
                    resBuffer.append(HEX_CHAR[(currChar >> 8) & 0xF]);  // Hex for the second group of 4-bits from the left.
                    resBuffer.append(HEX_CHAR[(currChar >> 4) & 0xF]);  // Hex for the third group.
                    resBuffer.append(HEX_CHAR[currChar & 0xF]);         // Hex for the last group, e.g., the right most 4-bits.
                } else {
                    resBuffer.append(currChar);
                }
            }
            res = resBuffer.toString();
        }
        return res;
    }
} public class Version {

	public static final int MAJOR = 3;
	public static final int MINOR = 0;
	public static final int PATCH = 8;
	public static final String VERSION = MAJOR + "." + MINOR + "." + PATCH;

	private Version() {
	}
} public class FileUtils {
    
    /** Suppresses default constructor, ensuring non-instantiability. */
    private FileUtils() {
    }
    
    /**
     * List all files and folders of a specific folder with a recursive search.
     *
     * @param jframe the frame to show progress.
     * @param files the collection of files and folder to complete with results.
     * @param folders the base paths to initiate research from.
     * @param depth the recursive search depth.
     * @param dangerousFolders list of folders patterns to exclude.
     */
    public static void listDir(JFrame jframe, Collection<File> files, Collection<File> folders, int depth,
                               final List<Pattern> dangerousFolders) {
        String frameTitle = jframe.getTitle();
        try {
            for (File folder : folders) {
                Log.info("Folder to scan: '" + folder.getAbsolutePath() + '\'');
                try {
									if (!folder.exists()) {
											Log.info("Skipped path: '" + folder.getAbsolutePath() + "', this path doesn't exist");
											continue;
									}
									if (!folder.isDirectory()) {
											Log.info("Skipped path: '" + folder.getAbsolutePath() + "', this path is not a directory");
											continue;
									}
                    files.addAll(org.apache.commons.io.FileUtils.listFiles(folder, null, false));
                    File[] subFolders;
                    subFolders = folder.listFiles((File pathname) -> {
                        boolean accept;
                        if (pathname.isDirectory()) {
                            accept = true;
                            for (Pattern dangerousPatt : dangerousFolders) {
                                if (StringHelper.checkRegex(pathname.getAbsolutePath(), dangerousPatt)) {
                                    accept = false;
                                    Log.info("Skipped hazardous place: '" + pathname + "'");
                                    break;
                                }
                            }
                        } else {
                            accept = false;
                        }
                        return accept;
                    });
                    if (depth > 0 && subFolders != null) {
                        for (File subFolder : subFolders) {
                            if (JFrameMain.isCLOSING_APP()) {
                                break;
                            }
                            files.add(subFolder);
                            listDirNoRecount(jframe, files, subFolder, depth - 1, dangerousFolders); // FIXME needs optimization (duplicated code: listDirNoRecount)
                        }
                    }
                } catch (Exception ex) {
                    Log.error(ex);
                }
            }
        } finally {
            jframe.setTitle(frameTitle);
        }
    }
    
    /**
     * List all files and folders of a specific folder with a recursive search.
     *
     * @param files the collection of files and folder to complete with results.
     * @param folder the base path to initiate research from.
     * @param depth the recursive search depth.
     * @param dangerousFolders list of folders patterns to exclude.
     */
    private static void listDirNoRecount(final JFrame jframe, Collection<File> files, File folder, int depth,
                                         final List<Pattern> dangerousFolders) {
        jframe.setTitle(folder.getAbsolutePath() + File.separatorChar);
        files.addAll(org.apache.commons.io.FileUtils.listFiles(folder, null, false));
        File[] subFolders = folder.listFiles((File pathname) -> {
            boolean accept;
            if (pathname.isDirectory()) {
                accept = true;
                for (Pattern dangerousPatt : dangerousFolders) {
                    if (StringHelper.checkRegex(pathname.getAbsolutePath(), dangerousPatt)) {
                        accept = false;
                        Log.info("Skipped hazardous place: '" + pathname + "'");
                        break;
                    }
                }
            } else {
                accept = false;
            }
            return accept;
        });
        if (depth > 0 && subFolders != null) {
            for (File subFolder : subFolders) { // FIXED avoid NPE on (protected) subFolders if custom dir.
                if (JFrameMain.isCLOSING_APP()) {
                    break;
                }
                files.add(subFolder);
                listDirNoRecount(jframe, files, subFolder, depth - 1, dangerousFolders);
            }
        }
    }
    
    /**
     * Check if a file-name verifies one of the patterns in the patterns-collection.
     *
     * @param file the file to check.
     * @param redistsPatterns the patterns-collection.
     * @return the file if it verifies a pattern, otherwise null.
     */
    public static Redist checkFile(File file, List<Redist> redistsPatterns) {
        Redist checkedFiles = null;
        for (Redist redist : redistsPatterns) {
            Pattern pattern = redist.getCompiledPattern();
            String fileName = file.getName();
            if (pattern.matcher(fileName).find()) {
                checkedFiles = new Redist(file, redist.getDescription());
                break;
            }
        }
        return checkedFiles;
    }
    
    /**
     * Delete a folder on the hard-drive.
     *
     * @param folder the folder to delete.
     * @return true if deletion is successful, otherwise false.
     */
    public static boolean deleteFolder(File folder) {
        boolean deleted;
        try {
            if (folder.exists()) {
                org.apache.commons.io.FileUtils.deleteDirectory(folder);
            }
            deleted = true;
        } catch (IOException ex) {
            Log.error(ex);
            deleted = false;
        }
        return deleted;
    }
} public class GraphicsUtils {
    
    private static final Toolkit DEFAULT_TOOLKIT = Toolkit.getDefaultToolkit();
    
    /** Suppresses default constructor, ensuring non-instantiability. */
    private GraphicsUtils() {
    }
    
    /**
     * Center a frame.
     *
     * @param window the frame to center.
     */
    public static void setFrameCentered(Window window) {
        Dimension screenSize = DEFAULT_TOOLKIT.getScreenSize();
        final int screenWidth = screenSize.width;
        final int screenHeight = screenSize.height;
        int posX = (screenWidth / 2) - (window.getWidth() / 2);
        int posY = (screenHeight / 2) - (window.getHeight() / 2);
        window.setBounds(posX, posY, window.getWidth(), window.getHeight());
    }
    
    /**
     * Set a frame's icon.
     *
     * @param window the frame to set icon.
     */
    public static void setIcon(Window window) {
        window.setIconImage(DEFAULT_TOOLKIT.createImage(GraphicsUtils.class.getResource(
                "/fr/tikione/steam/cleaner/gui/icons/tikione-steam-cleaner-icon.png")));
    }
} public class Log {
    
    /** Default logger. */
    private static Logger messagesLogger;
    
    static {
        try {
            Properties conf = new Properties();
            File backupLog4j = new File("conf/backup/tikione-steam-cleaner_log4j.properties");
            File userprofile = new File(Config.getProfilePath());
            //noinspection ResultOfMethodCallIgnored
            userprofile.mkdirs();
            File userLog4j = new File(userprofile.getAbsolutePath() + "/tikione-steam-cleaner_log4j.properties");
            if (!userLog4j.exists()) {
                org.apache.commons.io.FileUtils.copyFile(backupLog4j, userLog4j);
            }
            conf.load(new FileReader(userLog4j));
            conf.setProperty("log4j.appender.messages.File", Config.getProfilePath() + "/log/steamcleaner_messages.log");
            PropertyConfigurator.configure(conf);
            messagesLogger = Logger.getLogger("fr.tikione.steam.cleaner.log.info");
        } catch (IOException ex) {
            throw new RuntimeException("Cannot instantiate Log4j", ex);
        }
    }
    
    /** Suppresses default constructor, ensuring non-instantiability. */
    private Log() {
    }
    
    /**
     * Log a message object with the INFO Level.
     *
     * @param message the emssage to log.
     */
    public static void info(String message) {
        messagesLogger.info(message);
    }
    
    /**
     * Log a message object with the ERROR Level.
     *
     * @param ex the exception to log.
     */
    public static void error(Throwable ex) {
        messagesLogger.error("", ex);
    }
    
    /**
     * Log a message object with the ERROR Level.
     *
     * @param message the emssage to log.
     * @param ex the exception to log.
     */
    public static void error(String message, Throwable ex) {
        messagesLogger.error(message, ex);
    }
} @Getter
@EqualsAndHashCode(exclude = {"description", "compiledPattern"})
public class Redist {
    
    private final String description;
    
    private Pattern compiledPattern;
    
    private File file;
    
    /**
     * Define a file or folder name pattern that represents a single or a set of redistributable packages. Used to find
     * redistributable packages files and folders on the system storage.
     *
     * @param pattern the pattern (a regular expression) that represents the redistributable package.
     * @param description a description of the redistributable package.
     */
    public Redist(String pattern, String description) {
        this.description = description;
        this.compiledPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
    }
    
    /**
     * Define description of a redistributable package file or folder found on the system storage.
     *
     * @param file the redistributable package found on the system storage.
     * @param description a description of the redistributable package.
     */
    public Redist(File file, String description) {
        this.file = file;
        this.description = description;
    }
    
    public double getSize() {
        long fsize;
        if (file.isFile()) {
            fsize = org.apache.commons.io.FileUtils.sizeOf(file);
        } else if (file.isDirectory()) {
            fsize = org.apache.commons.io.FileUtils.sizeOfDirectory(file);
        } else {
            fsize = 0;
        }
        double floatSize = fsize;
        floatSize /= (1024.0 * 1024.0);
        return floatSize;
    }
} @SuppressWarnings("serial")
public class RedistTableModel extends DefaultTableModel {
    
    /** Table columns type. */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private final Class<Object>[] types = new Class[]{Boolean.class, String.class, Double.class, String.class};
    
    /** Indicates if table columns are editable. */
    private final boolean[] canEdit = new boolean[]{true, false, false, false};
    
    @Override
    public Class<Object> getColumnClass(int columnIndex) {
        return types[columnIndex];
    }
    
    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return canEdit[columnIndex];
    }
    
    /**
     * Table model for redistributable packages found.
     *
     * @param translation messages translation handler.
     */
    public RedistTableModel(Translation translation) {
        super(new Object[][]{}, new String[]{
                translation.getString("W_MAIN", "redistTable.col.title.select"),
                translation.getString("W_MAIN", "redistTable.col.title.path"),
                translation.getString("W_MAIN", "redistTable.col.title.size"),
                translation.getString("W_MAIN", "redistTable.col.title.title")});
    }
} public class Translation {
    
    private static final String DEFAULT_LANGCODE = "en";
    
    private static final String CONF_BASEPATH = "conf/i18n/";
    
    public static final String CONF_BASEPATH_FLAGS = CONF_BASEPATH + "flags/";
    
    private static final String CONF_EXT = ".ini";
    
    private Ini iniLang;
    
    public static final String SEC_WMAIN = "W_MAIN";
    
    public static final String SEC_WABOUT = "W_ABOUT";
    
    public static final String SEC_WCHECKFORUPDATES = "W_CHECKFORUPDATES";
    
    public static final String SEC_DELETE = "W_DELETE";
    
    public static final String SEC_OPTIONS = "W_OPTIONS";
    
    public Translation(String localeCode) {
        try {
            iniLang = new Ini();
            String lngEncoding = I18nEncoding.getInstance().getLngEncoding(localeCode);
            iniLang.load(new File(CONF_BASEPATH + localeCode + ".ini"), lngEncoding);
            Log.info("Loaded language '" + localeCode + "' with " + lngEncoding + " encoding");
        } catch (IOException | PatternSyntaxException | InfinitiveLoopException ex) {
            Log.error(ex);
        }
    }
    
    /**
     * Get a translated string denoted by an identifier and a section name.
     *
     * @param section the section name.
     * @param key the string identifier.
     * @return the translated string.
     */
    public String getString(String section, String key) {
        try {
            return iniLang.getKeyValue(" ??? ", section, key);
        } catch (CharConversionException | InfinitiveLoopException ex) {
            Log.error(ex);
            return " ??? ";
        }
    }
    
    public static List<CountryLanguage> getAvailLangList()
            throws IOException,
            InfinitiveLoopException {
        List<CountryLanguage> langList = new ArrayList<>(2);
        File i18nFolder = new File(CONF_BASEPATH);
        File[] langFiles = i18nFolder.listFiles((File dir, String name) -> name.endsWith(CONF_EXT) && !name.equalsIgnoreCase("encoding.ini"));
        Ini langIni = new Ini();
        if (langFiles != null) {
            for (File langFile : langFiles) {
                langIni.load(langFile, Main.CONF_ENCODING);
                String langDesc = langIni.getKeyValue(" ??? ", "", "name");
                String langFilename = langFile.getName();
                langFilename = langFilename.substring(0, langFilename.indexOf('.'));
                langList.add(new CountryLanguage(langFilename, langDesc));
            }
        }
        return langList;
    }
    
    @SuppressWarnings("CallToThreadDumpStack")
    public static String getSystemLangIfAvailable(List<CountryLanguage> availLangList) {
        String sysLang = DEFAULT_LANGCODE;
        try {
            String userLanguage = System.getProperty("user.language");
            String userCountry = System.getProperty("user.country");
            String userLocale = userLanguage + "_" + userCountry;
            for (CountryLanguage lang : availLangList) {
                String langCode = lang.getCode();
                if (langCode.equalsIgnoreCase(userLocale) || langCode.equalsIgnoreCase(userLanguage)) {
                    sysLang = langCode;
                    break;
                }
            }
        } catch (Exception ex) {
            Log.error(ex);
            sysLang = DEFAULT_LANGCODE;
        }
        return sysLang;
    }
} public class UpdateManager {
    
    /** Suppresses default constructor, ensuring non-instantiability. */
    private UpdateManager() {
    }
    
    /**
     * Get the latest program's online version number.
     *
     * @return latest version number.
     */
    public static String getLatestVersion() {
        String latestVersion;
        try {
            URL url = new URL(Config.getInstance().getLatestVersionUrl());
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            try (BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                String line = rd.readLine();
                if (line != null) {
                    latestVersion = line;
                    if (latestVersion.startsWith("fr.tikione.steam.cleaner=")) {
                        latestVersion = latestVersion.substring("fr.tikione.steam.cleaner=".length());
                    } else {
                        Log.error(new java.io.StreamCorruptedException("Bad online version file"));
                        latestVersion = Version.VERSION;
                    }
                } else {
                    Log.error(new java.io.StreamCorruptedException("Empty online version file"));
                    latestVersion = Version.VERSION;
                }
            }
        } catch (ConnectException | UnknownHostException ex) {
            Log.error("Cannot contact the update center", ex);
            latestVersion = Version.VERSION;
        } catch (InfinitiveLoopException | IOException ex) {
            Log.error(ex);
            latestVersion = Version.VERSION;
        }
        return latestVersion;
    }
    
    /**
     * Indicates if the current program's version number is up to date, compared to the given one.
     *
     * @param latestVersion version number to compare current with.
     * @return {@code true} if current version is up to date, otherwise {@code false}.
     */
    public static boolean IsUpToDate(String latestVersion) {
        int c1 = Version.MAJOR;
        int c2 = Version.MINOR;
        int c3 = Version.PATCH;
        String[] asOnlineVersion = normalizeVersionStr(latestVersion).split("\\.", -1);
        int o1 = Integer.parseInt(asOnlineVersion[0]);
        int o2 = Integer.parseInt(asOnlineVersion[1]);
        int o3 = Integer.parseInt(asOnlineVersion[2]);
        int iCurrentVersion = c3 + 1_000 * c2 + 1_000_000 * c1;
        int iOnlineVersion = o3 + 1_000 * o2 + 1_000_000 * o1;
        return iCurrentVersion >= iOnlineVersion;
    }
    
    /**
     * Normalize a version number. Per example, "01.08.10" becomes "1.8.10".
     *
     * @param version version number to normalize.
     * @return normalized version number.
     */
    public static String normalizeVersionStr(String version) {
        String[] currentVersion = version.split("\\.", -1);
        int v1 = Integer.parseInt(currentVersion[0]);
        int v2 = Integer.parseInt(currentVersion[1]);
        int v3 = Integer.parseInt(currentVersion[2]);
        return v1 + "." + v2 + "." + v3;
    }
    
    /**
     * Launch the external web browser with the TikiOne Steam Cleaner download URL (on the GitHub.com website).
     */
    public static void extBrowserGetLatestVersion() {
        extBrowser("https://github.com/jonathanlermitage/tikione-steam-cleaner/releases/latest");
    }
    
    /**
     * Launch the external web browser with the TikiOne Steam Cleaner changelog file URL (on the GitHub.com website).
     */
    public static void extBrowserGetLatestChangelog() {
        extBrowser("https://github.com/jonathanlermitage/tikione-steam-cleaner/blob/master/CHANGELOG.md");
    }
    
    /**
     * Launches the default browser to display a URI.
     *
     * @param uri the URI to display.
     */
    public static void extBrowser(String uri) {
        if (Desktop.isDesktopSupported()) {
            try {
                Desktop.getDesktop().browse(new URI(uri));
            } catch (URISyntaxException | IOException ex) {
                Log.error(ex);
            }
        }
    }
} public class Config {
    
    /** INI configuration file section : Steam folders. */
    private static final String CONFIG_STEAM_FOLDERS = "STEAM_FOLDERS";
    
    /** INI configuration file key : possible Steam folders. */
    private static final String CONFIG_STEAM_FOLDERS__POSSIBLE_DIRS = "possibleSteamPaths";
    
    /** INI configuration file key : latest Steam folder used in application. */
    private static final String CONFIG_STEAM_FOLDERS__LATEST_DIR = "latestSteamPath";
    
    /** INI configuration file key : max depth in folders recursive search. */
    private static final String CONFIG_STEAM_FOLDERS__MAX_DEPTH = "maxDepth";
    
    /** INI configuration file section : UI settings. */
    private static final String CONFIG_MAIN_WINDOW_UI = "MAIN_WINDOW_UI";
    
    /** INI configuration file key : latest Window length. */
    private static final String CONFIG_MAIN_WINDOW_UI__SIZE_X = "latestLength";
    
    /** INI configuration file key : latest Window height. */
    private static final String CONFIG_MAIN_WINDOW_UI__SIZE_Y = "latestHeight";
    
    /** INI configuration file key : indicate if window is maximized. */
    private static final String CONFIG_MAIN_WINDOW_UI__STATE = "state";
    
    /** INI configuration file section : UI language. */
    private static final String CONFIG_LANG = "LANG";
    
    /** INI configuration file key : the selected UI language. */
    private static final String CONFIG_LANG__SELECTION = "selected";
    
    /** INI configuration file section : misc. */
    private static final String CONFIG_MISC = "MISC";
    
    /** INI configuration file key : check for updates at startup. */
    private static final String CONFIG_MISC__CHK_FOR_UPT_AT_STATUP = "checkForUpdatesAtStartup";
    
    /** INI configuration file key : list of remote redist definition files. */
    private static final String CONFIG_MISC__REMOTE_DEFINITION_FILES = "remoteDefinitionFiles";
    
    /** Singleton handler. */
    private static final Config config;
    
    /** Default remote definition file. */
    private static final String DEFAULT_REMOTE_DEFINITION_FILE = "https://raw.githubusercontent.com/jonathanlermitage/"
            + "tikione-steam-cleaner/master/dist2/conf/backup/tikione-steam-cleaner_patterns.ini";
    
    private static final String CONFIG_FILENAME = "tikione-steam-cleaner_config_rev241.ini";
    
    /** File to use for configuration loading and saving. */
    private final File configFile;
    
    /** Configuration object. */
    private final Ini ini;
    
    private boolean updated = false;
    
    static {
        // Singleton creation.
        try {
            config = new Config();
        } catch (IOException | InfinitiveLoopException ex) {
            Log.error(ex);
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Get the configuration handler as a singleton.
     *
     * @return the configuration handler singleton.
     */
    public static synchronized Config getInstance() {
        return config;
    }
    
    /**
     * Load application configuration file. A default configuration file is created if necessary.
     *
     * @throws IOException if an I/O error occurs while retrieving the internal default configuration file, or while writing this default
     * configuration file.
     */
    private Config()
            throws IOException, InfinitiveLoopException {
        File backupConfigFile = new File("conf/backup/tikione-steam-cleaner_config.ini");
        File userprofile = new File(getProfilePath());
        //noinspection ResultOfMethodCallIgnored
        userprofile.mkdirs();
        configFile = new File(userprofile.getAbsolutePath() + "/" + CONFIG_FILENAME);
        if (!configFile.exists()) {
            org.apache.commons.io.FileUtils.copyFile(backupConfigFile, configFile);
        }
        ini = new Ini();
        ini.getConfig().enableParseLineConcat(true);
        ini.getConfig().enableReadUnicodeEscConv(true);
        ini.load(configFile, Main.CONF_ENCODING);
    }
    
    /**
     * Get profile path.
     * @return profile path.
     */
    public static String getProfilePath() {
        if (Main.ARG_PORTABLE) {
            return "./.tikione/";
        } else {
            return System.getenv("USERPROFILE") + "/.tikione/";
        }
    }
    
    /**
     * Saves the configuration into a file.
     *
     * @throws IOException if an I/O error occurs while writing the file.
     */
    public void save()
            throws IOException {
        if (updated) {
            ini.store(configFile, Main.CONF_ENCODING, Main.CONF_NEWLINE);
        }
    }
    
    public List<String> getPossibleSteamFolders()
            throws CharConversionException,
            InfinitiveLoopException {
        String folderList = ini.getKeyValue("/", CONFIG_STEAM_FOLDERS, CONFIG_STEAM_FOLDERS__POSSIBLE_DIRS);
        String[] patterns = folderList.split(Matcher.quoteReplacement(";"), 0);
        List<String> allSteamPaths = new ArrayList<>(80);
        File[] roots = File.listRoots();
        String[] driveLetters = new String[roots.length];
        for (int nRoot = 0; nRoot < roots.length; nRoot++) {
            driveLetters[nRoot] = roots[nRoot].getAbsolutePath();
        }
        String varDriveLetter = /* Matcher.quoteReplacement( */ "{drive_letter}"/* ) */;
        for (String basePattern : patterns) {
            for (String driveLetter : driveLetters) {
                allSteamPaths.add(basePattern.replace(varDriveLetter, driveLetter));
            }
        }
        Collections.addAll(allSteamPaths, patterns);
        return allSteamPaths;
    }
    
    public boolean getCheckForUpdatesAtStartup()
            throws CharConversionException,
            InfinitiveLoopException {
        return Boolean.parseBoolean(ini.getKeyValue("false", CONFIG_MISC, CONFIG_MISC__CHK_FOR_UPT_AT_STATUP));
    }
    
    public String getRemoteDefinitionFiles()
            throws CharConversionException,
            InfinitiveLoopException {
        return ini.getKeyValue(DEFAULT_REMOTE_DEFINITION_FILE, CONFIG_MISC, CONFIG_MISC__REMOTE_DEFINITION_FILES);
    }
    
    public String getLatestSteamFolder()
            throws CharConversionException,
            InfinitiveLoopException {
        return ini.getKeyValue("", CONFIG_STEAM_FOLDERS, CONFIG_STEAM_FOLDERS__LATEST_DIR);
    }
    
    public int getMaDepth()
            throws CharConversionException,
            InfinitiveLoopException {
        return Integer.parseInt(ini.getKeyValue("", CONFIG_STEAM_FOLDERS, CONFIG_STEAM_FOLDERS__MAX_DEPTH));
    }
    
    public int getUILatestWidth()
            throws CharConversionException,
            InfinitiveLoopException {
        return Integer.parseInt(ini.getKeyValue("750", CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__SIZE_X));
    }
    
    public int getUILatestHeight()
            throws CharConversionException,
            InfinitiveLoopException {
        return Integer.parseInt(ini.getKeyValue("500", CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__SIZE_Y));
    }
    
    public int getUIState()
            throws CharConversionException,
            InfinitiveLoopException {
        return Integer.parseInt(ini.getKeyValue("0", CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__STATE));
    }
    
    public void setCheckForUpdatesAtStartup(boolean checkForUpt) {
        updated = true;
        ini.setKeyValue(CONFIG_MISC, CONFIG_MISC__CHK_FOR_UPT_AT_STATUP, Boolean.toString(checkForUpt));
    }
    
    public void setRemoteDefinitionFiles(String urls) {
        updated = true;
        ini.setKeyValue(CONFIG_MISC, CONFIG_MISC__REMOTE_DEFINITION_FILES, urls);
    }
    
    public void setLatestSteamFolder(String folder) {
        updated = true;
        ini.setKeyValue(CONFIG_STEAM_FOLDERS, CONFIG_STEAM_FOLDERS__LATEST_DIR, folder);
    }
    
    public void setUILatestWidth(int width) {
        updated = true;
        ini.setKeyValue(CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__SIZE_X, Integer.toString(width));
    }
    
    public void setUILatestHeight(int height) {
        updated = true;
        ini.setKeyValue(CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__SIZE_Y, Integer.toString(height));
    }
    
    public void setUIState(int state) {
        updated = true;
        ini.setKeyValue(CONFIG_MAIN_WINDOW_UI, CONFIG_MAIN_WINDOW_UI__STATE, Integer.toString(state));
    }
    
    public void setMaxDepth(int maxDepth) {
        updated = true;
        ini.setKeyValue(CONFIG_STEAM_FOLDERS, CONFIG_STEAM_FOLDERS__MAX_DEPTH, Integer.toString(maxDepth));
    }
    
    public String getSelectedLanguage()
            throws
            InfinitiveLoopException,
            IOException {
        return getSelectedLanguage(Translation.getAvailLangList());
    }
    
    public String getSelectedLanguage(List<CountryLanguage> availLang)
            throws
            InfinitiveLoopException,
            IOException {
        String lang = ini.getKeyValue("", CONFIG_LANG, CONFIG_LANG__SELECTION);
        if (fr.tikione.ini.util.StringHelper.strIsEmpty(lang)) {
            lang = Translation.getSystemLangIfAvailable(availLang);
        }
        return lang;
    }
    
    public void setSelectedLanguage(String language) {
        updated = true;
        ini.setKeyValue(CONFIG_LANG, CONFIG_LANG__SELECTION, language);
    }
    
    public String getLatestVersionUrl()
            throws CharConversionException,
            InfinitiveLoopException {
        return "https://raw.githubusercontent.com/jonathanlermitage/tikione-steam-cleaner/master/uc/latest_version.txt";
    }
} public class CustomFolders {
    
    /** INI configuration file section : custom folders. */
    private static final String CONFIG_CUSTOM_FOLDERS = "CUSTOM_FOLDERS";
    
    /** INI configuration file key : custom folders. */
    private static final String CONFIG_CUSTOM_FOLDERS__ITEM_LIST = "itemList";
    
    /** File to use for configuration loading and saving. */
    private final File configFile;
    
    /** Configuration object. */
    private final Ini ini;
    
    private boolean updated = false;
    
    /**
     * Load application configuration file. A default configuration file is created if necessary.
     *
     * @throws IOException if an I/O error occurs while retrieving the internal default configuration file, or while writing this default
     *         configuration file.
     */
    public CustomFolders()
            throws IOException {
        File backupConfigFile = new File("conf/backup/tikione-steam-cleaner_custom-folders.ini");
        File userprofile = new File(Config.getProfilePath());
        //noinspection ResultOfMethodCallIgnored
        userprofile.mkdirs();
        configFile = new File(userprofile.getAbsolutePath() + "/tikione-steam-cleaner_custom-folders.ini");
        if (!configFile.exists()) {
            org.apache.commons.io.FileUtils.copyFile(backupConfigFile, configFile);
        }
        ini = new Ini();
        ini.getConfig().enableParseLineConcat(false);
        ini.getConfig().enableReadUnicodeEscConv(false);
        ini.load(configFile, Main.CONF_ENCODING);
    }
    
    /**
     * Saves the unchecked items list into a file.
     *
     * @throws IOException if an I/O error occurs while writing the file.
     */
    public void save()
            throws IOException {
        if (updated || !configFile.exists()) {
            ini.store(configFile, Main.CONF_ENCODING, Main.CONF_NEWLINE);
        }
    }
    
    /**
     * Get the list of unchecked items memorized from the file.
     *
     * @return the list of unchecked items.
     * @throws CharConversionException if an error occurs while retrieving the list from the file (invalid characters).
     * @throws InfinitiveLoopException if an error occurs while retrieving the list from the file (file parsing error).
     */
    @SuppressWarnings("unchecked")
    public List<String> getCustomFolders()
            throws CharConversionException,
            InfinitiveLoopException {
        String itemTable = ini.getKeyValue(null, CONFIG_CUSTOM_FOLDERS, CONFIG_CUSTOM_FOLDERS__ITEM_LIST);
        List<String> res;
        if (itemTable == null) {
            res = Collections.EMPTY_LIST;
        } else {
            res = Arrays.asList(itemTable.split(Matcher.quoteReplacement("\""), 0));
        }
        return res;
    }
    
    /**
     * Set the list of unchecked items to memorize.
     *
     * @param items the items to memorize.
     * @throws CharConversionException if an error occurs while setting the list from the file (invalid characters).
     * @throws InfinitiveLoopException if an error occurs while setting the list from the file (file parsing error).
     */
    @SuppressWarnings("Duplicates")
    public void setCustomFolders(List<String> items)
            throws CharConversionException,
            InfinitiveLoopException {
        List<String> prevItems = getCustomFolders();
        if (!prevItems.containsAll(items) || !items.containsAll(prevItems)) {
            updated = true;
            StringBuilder buffer = new StringBuilder(512);
            boolean first = true;
            for (String item : items) {
                if (first) {
                    buffer.append(item);
                } else {
                    buffer.append('\"').append(item);
                }
                first = false;
            }
            ini.setKeyValue(CONFIG_CUSTOM_FOLDERS, CONFIG_CUSTOM_FOLDERS__ITEM_LIST, buffer.toString());
        }
    }
} public class DangerousItems {
    
    private static final DangerousItems dangerousItems;
    
    /** INI configuration file section : unchecked items. */
    private static final String CONFIG_AUTOEXCLUDE_PATTERNS = "AUTOEXCLUDE_PATTERNS";
    
    /** INI configuration file key : unchecked items. */
    private static final String CONFIG_AUTOEXCLUDE_PATTERNS__FOLDERS_LIST = "folderPatterns";
    
    /** Configuration object. */
    private static Ini ini;
    
    static {
        // Singleton creation.
        try {
            dangerousItems = new DangerousItems();
        } catch (IOException ex) {
            Log.error(ex);
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Get the configuration handler as a singleton.
     *
     * @return the configuration handler singleton.
     */
    public static synchronized DangerousItems getInstance() {
        return dangerousItems;
    }
    
    /**
     * Load application configuration file.
     *
     * @throws IOException if an I/O error occurs while retrieving the internal default configuration file.
     */
    private DangerousItems()
            throws IOException {
        ini = new Ini();
        ini.getConfig().enableParseLineConcat(true);
        ini.getConfig().enableReadUnicodeEscConv(true);
        ini.load(new File("conf/tikione-steam-cleaner_dangerous-items.ini"), Main.CONF_ENCODING);
    }
    
    public List<Pattern> getDangerousFolders()
            throws CharConversionException,
            InfinitiveLoopException {
        List<Pattern> dangerousFolders = new ArrayList<>(16);
        String[] keys = ini.getKeyValue("", CONFIG_AUTOEXCLUDE_PATTERNS, CONFIG_AUTOEXCLUDE_PATTERNS__FOLDERS_LIST).split("\"", 0);
        for (String pattern : keys) {
            if (pattern.length() > 0) {
                try {
                    dangerousFolders.add(Pattern.compile(pattern, Pattern.CASE_INSENSITIVE));
                } catch (PatternSyntaxException ex) {
                    Log.error(ex);
                }
            }
        }
        return dangerousFolders;
    }
} public class Patterns {
    
    public static final String REMOTE_DEFINITION_FILES_SEPARATOR = "##n##";
    
    /** INI configuration file section : redistributable packages file and folder patterns. */
    private static final String CONFIG_REDIST_PATTERNS = "REDISTRIBUTABLE_PACKAGES_PATTERNS";
    
    /** INI configuration file key : redistributable package file patterns. */
    private static final String CONFIG_REDIST_PATTERNS__FILE_PATTERNS = "redistFilePatterns";
    
    /** INI configuration file key : redistributable package folder patterns. */
    private static final String CONFIG_REDIST_PATTERNS__FOLDER_PATTERNS = "redistFolderPatterns";
    
    /** INI configuration file key : redistributable package file patterns. */
    private static final String CONFIG_REDIST_PATTERNS__FILE_PATTERNS_EXP = "experimentalRedistFilesPatterns";
    
    /** INI configuration file key : redistributable package folder patterns. */
    private static final String CONFIG_REDIST_PATTERNS__FOLDER_PATTERNS_EXP = "experimentalRedistFolderPatterns";
    
    /** INI configuration file key : enable experimental patterns. */
    private static final String CONFIG_REDIST_PATTERNS__ENABLE_EXP_PATTERNS = "enableExperimentalPatterns";
    
    private static final String CONFIG_FILENAME = "tikione-steam-cleaner_patterns_rev244.ini";
    
    /** Singleton handler. */
    private static final Patterns config;
    
    /** File to use for configuration loading and saving. */
    private final File configFile;
    
    /** Configuration object. */
    private Ini ini;
    
    private List<Ini> inis;
    
    private boolean updated = false;
    
    static {
        // Singleton creation.
        try {
            config = new Patterns();
        } catch (IOException | InfinitiveLoopException ex) {
            Log.error(ex);
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Get the configuration handler as a singleton.
     *
     * @return the configuration handler singleton.
     */
    public static synchronized Patterns getInstance() {
        return config;
    }
    
    /**
     * Load application configuration file. A default configuration file is created if necessary.
     *
     * @throws IOException if an I/O error occurs while retrieving the internal default configuration file, or while writing this default
     * configuration file.
     */
    @SuppressWarnings("OverridableMethodCallInConstructor")
    private Patterns()
            throws IOException, InfinitiveLoopException {
        File backupConfigFile = new File("conf/backup/tikione-steam-cleaner_patterns.ini");
        File userprofile = new File(Config.getProfilePath());
        //noinspection ResultOfMethodCallIgnored
        userprofile.mkdirs();
        configFile = new File(userprofile.getAbsolutePath() + "/" + CONFIG_FILENAME);
        if (!configFile.exists()) {
            org.apache.commons.io.FileUtils.copyFile(backupConfigFile, configFile);
        }
        
        reload();
    }
    
    public void reload()
            throws IOException, InfinitiveLoopException {
        ini = new Ini();
        ini.getConfig().enableParseLineConcat(true);
        ini.getConfig().enableReadUnicodeEscConv(true);
        ini.load(configFile, Main.CONF_ENCODING);
        
        inis = new ArrayList<>(8);
        inis.add(ini);
        inis.addAll(RemotePatterns.getInis());
    }
    
    /**
     * Saves the configuration into a file.
     *
     * @throws IOException if an I/O error occurs while writing the file.
     */
    public void save()
            throws IOException {
        if (updated) {
            ini.store(configFile, Main.CONF_ENCODING, Main.CONF_NEWLINE);
        }
    }
    
    public List<Redist> getRedistFilePatternsAndDesc(boolean includeExp)
            throws CharConversionException,
            InfinitiveLoopException {
        List<Redist> res = getRedistPatternsAndDesc(CONFIG_REDIST_PATTERNS__FILE_PATTERNS);
        if (includeExp) {
            List<Redist> resAdd = getRedistPatternsAndDesc(CONFIG_REDIST_PATTERNS__FILE_PATTERNS_EXP);
            res.addAll(resAdd);
        }
        return res;
    }
    
    public List<Redist> getRedistFolderPatternsAndDesc(boolean includeExp)
            throws CharConversionException,
            InfinitiveLoopException {
        List<Redist> res = getRedistPatternsAndDesc(CONFIG_REDIST_PATTERNS__FOLDER_PATTERNS);
        if (includeExp) {
            List<Redist> resAdd = getRedistPatternsAndDesc(CONFIG_REDIST_PATTERNS__FOLDER_PATTERNS_EXP);
            res.addAll(resAdd);
        }
        return res;
    }
    
    public boolean getEnableExperimentalPatterns()
            throws CharConversionException,
            InfinitiveLoopException {
        return Boolean.parseBoolean(ini.getKeyValue("false", CONFIG_REDIST_PATTERNS, CONFIG_REDIST_PATTERNS__ENABLE_EXP_PATTERNS));
    }
    
    private List<Redist> getRedistPatternsAndDesc(String configKey)
            throws CharConversionException,
            InfinitiveLoopException {
        List<Redist> redistList = new ArrayList<>(8);
        List<String> redistTokens = new ArrayList<>(64);
        for (Ini singleIni : inis) {
            String[] tokens = singleIni.getKeyValue("", CONFIG_REDIST_PATTERNS, configKey).split("\"", 0);
            redistTokens.addAll(Arrays.asList(tokens));
        }
        boolean FileDescToggle = true;
        String redistName = null;
        String redtsiDescription;
        for (String redist : redistTokens) {
            if (redist.length() > 0) { // Skip first and last tokens.
                if (FileDescToggle) {
                    redistName = redist;
                } else {
                    redtsiDescription = redist;
                    redistList.add(new Redist(redistName, redtsiDescription));
                }
                FileDescToggle ^= true;
            }
        }
        return redistList;
    }
    
    public void setEnableExperimentalPatterns(boolean enable) {
        updated = true;
        ini.setKeyValue(CONFIG_REDIST_PATTERNS, CONFIG_REDIST_PATTERNS__ENABLE_EXP_PATTERNS, Boolean.toString(enable));
    }
} public class UncheckedItems {
    
    /** INI configuration file section : unchecked items. */
    private static final String CONFIG_UNCHECKED_REDIST_ITEMS = "UNCHECKED_REDIST_ITEMS";
    
    /** INI configuration file key : unchecked items. */
    private static final String CONFIG_UNCHECKED_REDIST_ITEMS__ITEM_LIST = "itemList";
    
    /** File to use for configuration loading and saving. */
    private final File configFile;
    
    /** Configuration object. */
    private final Ini ini;
    
    private boolean updated = false;
    
    /**
     * Load application configuration file. A default configuration file is created if necessary.
     *
     * @throws IOException if an I/O error occurs while retrieving the internal default configuration file, or while writing this default
     *         configuration file.
     */
    public UncheckedItems()
            throws IOException {
        File backupConfigFile = new File("conf/backup/tikione-steam-cleaner_unchecked-items.ini");
        File userprofile = new File(Config.getProfilePath());
        //noinspection ResultOfMethodCallIgnored
        userprofile.mkdirs();
        configFile = new File(userprofile.getAbsolutePath() + "/tikione-steam-cleaner_unchecked-items.ini");
        if (!configFile.exists()) {
            org.apache.commons.io.FileUtils.copyFile(backupConfigFile, configFile);
        }
        ini = new Ini();
        ini.getConfig().enableParseLineConcat(false);
        ini.getConfig().enableReadUnicodeEscConv(false);
        ini.load(configFile, Main.CONF_ENCODING);
    }
    
    /**
     * Saves the unchecked items list into a file.
     *
     * @throws IOException if an I/O error occurs while writing the file.
     */
    public void save()
            throws IOException {
        if (updated || !configFile.exists()) {
            ini.store(configFile, Main.CONF_ENCODING, Main.CONF_NEWLINE);
        }
    }
    
    /**
     * Get the list of unchecked items memorized from the file.
     *
     * @return the list of unchecked items.
     * @throws CharConversionException if an error occurs while retrieving the list from the file (invalid characters).
     * @throws InfinitiveLoopException if an error occurs while retrieving the list from the file (file parsing error).
     */
    @SuppressWarnings("unchecked")
    public List<String> getUncheckedItems()
            throws CharConversionException,
            InfinitiveLoopException {
        String itemTable = ini.getKeyValue(null, CONFIG_UNCHECKED_REDIST_ITEMS, CONFIG_UNCHECKED_REDIST_ITEMS__ITEM_LIST);
        List<String> res;
        if (itemTable == null) {
            res = Collections.EMPTY_LIST;
        } else {
            res = Arrays.asList(itemTable.split(Matcher.quoteReplacement("\""), 0));
        }
        return res;
    }
    
    /**
     * Set the list of unchecked items to memorize.
     *
     * @param items the items to memorize.
     * @throws CharConversionException if an error occurs while setting the list from the file (invalid characters).
     * @throws InfinitiveLoopException if an error occurs while setting the list from the file (file parsing error).
     */
    @SuppressWarnings("Duplicates")
    public void setUncheckedItems(List<String> items)
            throws CharConversionException,
            InfinitiveLoopException {
        List<String> prevItems = getUncheckedItems();
        if (!prevItems.containsAll(items) || !items.containsAll(prevItems)) {
            updated = true;
            StringBuilder buffer = new StringBuilder(512);
            boolean first = true;
            for (String item : items) {
                if (first) {
                    buffer.append(item);
                } else {
                    buffer.append('\"').append(item);
                }
                first = false;
            }
            ini.setKeyValue(CONFIG_UNCHECKED_REDIST_ITEMS, CONFIG_UNCHECKED_REDIST_ITEMS__ITEM_LIST, buffer.toString());
        }
    }
}
import fr.tikione.ini.InfinitiveLoopException;
import fr.tikione.ini.util.StringHelper;
import fr.tikione.steam.cleaner.Version;
import fr.tikione.steam.cleaner.util.FileComparator;
import fr.tikione.steam.cleaner.util.FileUtils;
import fr.tikione.steam.cleaner.util.GraphicsUtils;
import fr.tikione.steam.cleaner.util.Log;
import fr.tikione.steam.cleaner.util.Redist;
import fr.tikione.steam.cleaner.util.RedistTableModel;
import fr.tikione.steam.cleaner.util.Translation;
import fr.tikione.steam.cleaner.util.UpdateManager;
import fr.tikione.steam.cleaner.util.conf.Config;
import fr.tikione.steam.cleaner.util.conf.CustomFolders;
import fr.tikione.steam.cleaner.util.conf.DangerousItems;
import fr.tikione.steam.cleaner.util.conf.Patterns;
import fr.tikione.steam.cleaner.util.conf.UncheckedItems;
import javax.swing.*;
import javax.swing.GroupLayout.Alignment;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.io.CharConversionException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
	}

	/**
	 * Get an array of File objetcs from the list of custom folders.
	 *
	 * @return an array of folders.
	 */
	private File[] customFoldersListStrToFiles() {
		int nbFolders = listModel.size();
		List<File> folders = new ArrayList<>(nbFolders + 1);
		for (int i = 0; i < nbFolders; i++) {
			folders.add(listModel.get(i));
		}
		return folders.toArray(new File[folders.size()]);
	}

	/**
	 * Save the list of custom folders to a configuration file.
	 */
	private void memorizeCustomFoldersToConf() {
		try {
			File[] folders = customFoldersListStrToFiles();
			List<String> foldersAsStr = new ArrayList<>(folders.length + 1);
			for (File folder : folders) {
				foldersAsStr.add(folder.getAbsolutePath());
			}
			customFolders.setCustomFolders(foldersAsStr);
		} catch (CharConversionException | InfinitiveLoopException ex) {
			Log.error(ex);
		}
	}
	
	private void recomputeTotalSizeMB() {
		float fsize = 0;
		for (int i = 0; i < model.getRowCount(); i++) {
			if (Boolean.parseBoolean(model.getValueAt(i, 0).toString())) {
				fsize += Float.parseFloat(model.getValueAt(i, 2).toString());
			}
		}
		String btnLabel = translation.getString(Translation.SEC_WMAIN, "button.removeSelectedItems");
		jButtonRemoveRedistItemsFromDisk.setText(btnLabel + " (" + (int)fsize + "MB)");
	}

	/**
	 * Store the list of unchecked items (redistributable packages shown in the main table) to a configuration file.
	 */
	private void memorizeUncheckedItemsToConf() {
		try {
			if (fSteamDir != null) {
				List<String> nowUncheckedItemsList = new ArrayList<>(8);
				List<String> nowCheckedItemsList = new ArrayList<>(8);
				List<String> newUncheckedItemsList = new ArrayList<>(8);
				List<String> prevUncheckedItems = uncheckedItems.getUncheckedItems();
				for (int row = 0; row < model.getRowCount(); row++) {
					boolean checked = (Boolean) model.getValueAt(row, 0);
					String redistFullPath = (String) model.getValueAt(row, 1);
					if (checked) {
						nowCheckedItemsList.add(redistFullPath);
					} else {
						nowUncheckedItemsList.add(redistFullPath);
					}
				}
				newUncheckedItemsList.addAll(nowUncheckedItemsList);
				for (String prevUncheckedItem : prevUncheckedItems) {
					if (!nowCheckedItemsList.contains(prevUncheckedItem) && !newUncheckedItemsList.contains(prevUncheckedItem)) {
						newUncheckedItemsList.add(prevUncheckedItem);
					}
				}
				uncheckedItems.setUncheckedItems(newUncheckedItemsList);
			}
		} catch (CharConversionException | InfinitiveLoopException ex) {
			Log.error(ex);
		}
	}

	private void searchRedistPackagesOnDisk(final JButton buttonReload) {
		buttonReload.setEnabled(false);
		memorizeUncheckedItemsToConf();
		jPanelList.setBorder(BorderFactory.createTitledBorder(tblRedistLabelDefault));
		String sSteamDir = jTextFieldSteamDir.getText();
		if (!listModel.isEmpty() || !StringHelper.strIsEmpty(sSteamDir)) {
			if (!StringHelper.strIsEmpty(sSteamDir) && !sSteamDir.endsWith("/") && !sSteamDir.endsWith(File.separator)) {
				sSteamDir += File.separatorChar;
			}
			fSteamDir = new File(sSteamDir);
			
			// go to steamapps folder if exists (fix https://github.com/jonathanlermitage/tikione-steam-cleaner/issues/27)
			if (Objects.requireNonNull(fSteamDir.listFiles((dir, name) -> name.equalsIgnoreCase("steamapps"))).length > 0) {
				fSteamDir = new File(fSteamDir, "steamapps/");
				Log.info("steamapps folder found");
			}
			
			boolean steamExists = fSteamDir.isDirectory() && fSteamDir.exists();
			if (!listModel.isEmpty() || steamExists) {
				enableAllUI(false);
				buttonReload.setText(btnReloadLabelWorking);
				final JFrame thisframe = this;
				Thread tJob = new Thread(() -> {
					try {
						jButtonStopSearch.setVisible(true);
						String nameFile = translation.getString(Translation.SEC_WMAIN, "redistList.item.file");
						String nameFileUpCase = translation.getString(Translation.SEC_WMAIN, "redistList.item.fileUpperCase");
						String nameFiles = translation.getString(Translation.SEC_WMAIN, "redistList.item.files");
						String nameFolder = translation.getString(Translation.SEC_WMAIN, "redistList.item.folder");
						String nameFolderUpCase = translation.getString(Translation.SEC_WMAIN, "redistList.item.folderUppercase");
						String nameFolders = translation.getString(Translation.SEC_WMAIN, "redistList.item.folders");
						model = new RedistTableModel(translation);
						setTableModelUI();
						List<File> srcFolders = new ArrayList<>(32);
						if (fSteamDir.exists()) {
							srcFolders.add(fSteamDir);
						}
						File[] customFolders1 = customFoldersListStrToFiles();
						for (File customFolder : customFolders1) {
							if (!srcFolders.contains(customFolder)) {
								srcFolders.add(customFolder);
							}
						}
						List<File> allFiles = new ArrayList<>(1024);
						FileUtils.listDir(thisframe, allFiles, srcFolders, config.getMaDepth(), dangerousFolders);
						List<Redist> checkedFiles = new ArrayList<>(128);