forge.gui.ImportSourceAnalyzer.java Source code

Java tutorial

Introduction

Here is the source code for forge.gui.ImportSourceAnalyzer.java

Source

/*
 * Forge: Play Magic: the Gathering.
 * Copyright (c) 2013  Forge Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package forge.gui;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.card.CardEdition;
import forge.card.CardRules;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.model.FModel;
import forge.properties.ForgeConstants;
import forge.util.FileUtil;
import forge.util.ImageUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.io.File;
import java.util.*;

public class ImportSourceAnalyzer {

    public enum OpType {
        CONSTRUCTED_DECK, DRAFT_DECK, PLANAR_DECK, SCHEME_DECK, SEALED_DECK, UNKNOWN_DECK, DEFAULT_CARD_PIC, SET_CARD_PIC, POSSIBLE_SET_CARD_PIC, TOKEN_PIC, QUEST_PIC, GAUNTLET_DATA, QUEST_DATA, PREFERENCE_FILE, DB_FILE
    }

    public interface AnalysisCallback {
        boolean checkCancel();

        void addOp(OpType type, File src, File dest);
    }

    private final File source;
    private final AnalysisCallback cb;
    private final int numFilesToAnalyze;

    private int numFilesAnalyzed;

    public ImportSourceAnalyzer(final String source, final AnalysisCallback cb) {
        this.source = new File(source);
        this.cb = cb;
        numFilesToAnalyze = countFiles(this.source);
    }

    public int getNumFilesToAnalyze() {
        return numFilesToAnalyze;
    }

    public int getNumFilesAnalyzed() {
        return numFilesAnalyzed;
    }

    public void doAnalysis() {
        identifyAndAnalyze(this.source);
    }

    private void identifyAndAnalyze(final File root) {
        // see if we can figure out the likely identity of the source folder and
        // dispatch to the best analysis subroutine to handle it
        final String dirname = root.getName();

        if ("res".equalsIgnoreCase(dirname)) {
            analyzeOldResDir(root);
        } else if ("constructed".equalsIgnoreCase(dirname)) {
            analyzeConstructedDeckDir(root);
        } else if ("draft".equalsIgnoreCase(dirname)) {
            analyzeDraftDeckDir(root);
        } else if ("plane".equalsIgnoreCase(dirname) || "planar".equalsIgnoreCase(dirname)) {
            analyzePlanarDeckDir(root);
        } else if ("scheme".equalsIgnoreCase(dirname)) {
            analyzeSchemeDeckDir(root);
        } else if ("sealed".equalsIgnoreCase(dirname)) {
            analyzeSealedDeckDir(root);
        } else if (StringUtils.containsIgnoreCase(dirname, "deck")) {
            analyzeDecksDir(root);
        } else if ("gauntlet".equalsIgnoreCase(dirname)) {
            analyzeGauntletDataDir(root);
        } else if ("layouts".equalsIgnoreCase(dirname)) {
            analyzeLayoutsDir(root);
        } else if ("pics".equalsIgnoreCase(dirname)) {
            analyzeCardPicsDir(root);
        } else if ("pics_product".equalsIgnoreCase(dirname)) {
            analyzeProductPicsDir(root);
        } else if ("preferences".equalsIgnoreCase(dirname)) {
            analyzePreferencesDir(root);
        } else if ("quest".equalsIgnoreCase(dirname)) {
            analyzeQuestDir(root);
        } else if (null != FModel.getMagicDb().getEditions().get(dirname)) {
            analyzeCardPicsSetDir(root);
        } else {
            // look at files in directory and make a semi-educated guess based on file extensions
            int numUnhandledFiles = 0;
            File[] files = root.listFiles();
            assert files != null;
            for (final File file : files) {
                if (cb.checkCancel()) {
                    return;
                }

                if (file.isFile()) {
                    final String filename = file.getName();
                    if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
                        analyzeDecksDir(root);
                        numUnhandledFiles = 0;
                        break;
                    } else if (StringUtils.endsWithIgnoreCase(filename, ".jpg")) {
                        analyzeCardPicsDir(root);
                        numUnhandledFiles = 0;
                        break;
                    }

                    ++numUnhandledFiles;
                } else if (file.isDirectory()) {
                    identifyAndAnalyze(file);
                }
            }
            numFilesAnalyzed += numUnhandledFiles;
        }
    }

    //////////////////////////////////////////////////////////////////////////
    // pre-profile res dir
    //

    private void analyzeOldResDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            boolean onDir(final File dir) {
                final String dirname = dir.getName();
                if ("decks".equalsIgnoreCase(dirname)) {
                    analyzeDecksDir(dir);
                } else if ("gauntlet".equalsIgnoreCase(dirname)) {
                    analyzeGauntletDataDir(dir);
                } else if ("layouts".equalsIgnoreCase(dirname)) {
                    analyzeLayoutsDir(dir);
                } else if ("pics".equalsIgnoreCase(dirname)) {
                    analyzeCardPicsDir(dir);
                } else if ("pics_product".equalsIgnoreCase(dirname)) {
                    analyzeProductPicsDir(dir);
                } else if ("preferences".equalsIgnoreCase(dirname)) {
                    analyzePreferencesDir(dir);
                } else if ("quest".equalsIgnoreCase(dirname)) {
                    analyzeQuestDir(dir);
                } else {
                    return false;
                }
                return true;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // decks
    //

    private void analyzeDecksDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                // we don't really expect any files in here, but if we find a .dck file, add it to the unknown list
                final String filename = file.getName();
                if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
                    final File targetFile = new File(lcaseExt(filename));
                    cb.addOp(OpType.UNKNOWN_DECK, file, targetFile);
                }
            }

            @Override
            boolean onDir(final File dir) {
                final String dirname = dir.getName();
                if ("constructed".equalsIgnoreCase(dirname)) {
                    analyzeConstructedDeckDir(dir);
                } else if ("cube".equalsIgnoreCase(dirname)) {
                    return false;
                } else if ("draft".equalsIgnoreCase(dirname)) {
                    analyzeDraftDeckDir(dir);
                } else if ("plane".equalsIgnoreCase(dirname) || "planar".equalsIgnoreCase(dirname)) {
                    analyzePlanarDeckDir(dir);
                } else if ("scheme".equalsIgnoreCase(dirname)) {
                    analyzeSchemeDeckDir(dir);
                } else if ("sealed".equalsIgnoreCase(dirname)) {
                    analyzeSealedDeckDir(dir);
                } else {
                    analyzeKnownDeckDir(dir, null, OpType.UNKNOWN_DECK);
                }
                return true;
            }
        });
    }

    private void analyzeConstructedDeckDir(final File root) {
        analyzeKnownDeckDir(root, ForgeConstants.DECK_CONSTRUCTED_DIR, OpType.CONSTRUCTED_DECK);
    }

    private void analyzeDraftDeckDir(final File root) {
        analyzeKnownDeckDir(root, ForgeConstants.DECK_DRAFT_DIR, OpType.DRAFT_DECK);
    }

    private void analyzePlanarDeckDir(final File root) {
        analyzeKnownDeckDir(root, ForgeConstants.DECK_PLANE_DIR, OpType.PLANAR_DECK);
    }

    private void analyzeSchemeDeckDir(final File root) {
        analyzeKnownDeckDir(root, ForgeConstants.DECK_SCHEME_DIR, OpType.SCHEME_DECK);
    }

    private void analyzeSealedDeckDir(final File root) {
        analyzeKnownDeckDir(root, ForgeConstants.DECK_SEALED_DIR, OpType.SEALED_DECK);
    }

    private void analyzeKnownDeckDir(final File root, final String targetDir, final OpType opType) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                final String filename = file.getName();
                if (StringUtils.endsWithIgnoreCase(filename, ".dck")) {
                    final File targetFile = new File(targetDir, lcaseExt(filename));
                    if (!file.equals(targetFile)) {
                        cb.addOp(opType, file, targetFile);
                    }
                }
            }

            @Override
            boolean onDir(final File dir) {
                // if there's a dir beneath a known directory, assume the same kind of decks are in there
                analyzeKnownDeckDir(dir, targetDir, opType);
                return true;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // gauntlet
    //

    private void analyzeGauntletDataDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                // find *.dat files, but exclude LOCKED_*
                final String filename = file.getName();
                if (StringUtils.endsWithIgnoreCase(filename, ".dat") && !filename.startsWith("LOCKED_")) {
                    final File targetFile = new File(ForgeConstants.GAUNTLET_DIR.userPrefLoc, lcaseExt(filename));
                    if (!file.equals(targetFile)) {
                        cb.addOp(OpType.GAUNTLET_DATA, file, targetFile);
                    }
                }
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // layouts
    //

    private void analyzeLayoutsDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                // find *_preferred.xml files
                final String filename = file.getName();
                if (StringUtils.endsWithIgnoreCase(filename, "_preferred.xml")) {
                    final File targetFile = new File(ForgeConstants.USER_PREFS_DIR,
                            file.getName().toLowerCase(Locale.ENGLISH).replace("_preferred", ""));
                    cb.addOp(OpType.PREFERENCE_FILE, file, targetFile);
                }
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // default card pics
    //

    private static String oldCleanString(final String in) {
        final StringBuilder out = new StringBuilder();
        for (int i = 0; i < in.length(); i++) {
            final char c = in.charAt(i);
            if ((c == ' ') || (c == '-')) {
                out.append('_');
            } else if (Character.isLetterOrDigit(c) || (c == '_')) {
                out.append(c);
            }
        }

        // usually we would want to pass Locale.ENGLISH to the toLowerCase() method to prevent unintentional
        // character mangling on some system locales, but we want to replicate the old code here exactly
        return out.toString().toLowerCase();
    }

    private void addDefaultPicNames(final PaperCard c, final boolean backFace) {
        final CardRules card = c.getRules();
        final String urls = card.getPictureUrl(backFace);
        if (StringUtils.isEmpty(urls)) {
            return;
        }

        final int numPics = 1 + StringUtils.countMatches(urls, "\\");
        if (c.getArtIndex() > numPics) {
            return;
        }

        final String filenameBase = ImageUtil.getImageKey(c, backFace, false);
        final String filename = filenameBase + ".jpg";
        final boolean alreadyHadIt = null != defaultPicNames.put(filename, filename);
        if (alreadyHadIt) {
            return;
        }

        // Do you shift artIndex by one here?
        final String newLastSymbol = 0 == c.getArtIndex() ? "" : String.valueOf(c.getArtIndex() /* + 1 */);
        final String oldFilename = oldCleanString(filenameBase.replaceAll("[0-9]?(\\.full)?$", "")) + newLastSymbol
                + ".jpg";
        //if ( numPics > 1 )
        //System.out.printf("Will move %s -> %s%n", oldFilename, filename);
        defaultPicOldNameToCurrentName.put(oldFilename, filename);
    }

    private Map<String, String> defaultPicNames;
    private Map<String, String> defaultPicOldNameToCurrentName;

    private void analyzeCardPicsDir(final File root) {
        if (null == defaultPicNames) {
            defaultPicNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            defaultPicOldNameToCurrentName = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

            for (final PaperCard c : FModel.getMagicDb().getCommonCards().getAllCards()) {
                addDefaultPicNames(c, false);
                if (ImageUtil.hasBackFacePicture(c)) {
                    addDefaultPicNames(c, true);
                }
            }

            for (final PaperCard c : FModel.getMagicDb().getVariantCards().getAllCards()) {
                addDefaultPicNames(c, false);
                // variants never have backfaces
            }
        }

        analyzeListedDir(root, ForgeConstants.CACHE_CARD_PICS_DIR, new ListedAnalyzer() {
            @Override
            public String map(final String filename) {
                if (defaultPicOldNameToCurrentName.containsKey(filename)) {
                    return defaultPicOldNameToCurrentName.get(filename);
                }
                return defaultPicNames.get(filename);
            }

            @Override
            public OpType getOpType(final String filename) {
                return OpType.DEFAULT_CARD_PIC;
            }

            @Override
            boolean onDir(final File dir) {
                if ("icons".equalsIgnoreCase(dir.getName())) {
                    analyzeIconsPicsDir(dir);
                } else if ("tokens".equalsIgnoreCase(dir.getName())) {
                    analyzeTokenPicsDir(dir);
                } else {
                    analyzeCardPicsSetDir(dir);
                }
                return true;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // set card pics
    //

    private static void addSetCards(final Map<String, String> cardFileNames, final Iterable<PaperCard> library,
            final Predicate<PaperCard> filter) {
        for (final PaperCard c : Iterables.filter(library, filter)) {
            String filename = ImageUtil.getImageKey(c, false, true) + ".jpg";
            cardFileNames.put(filename, filename);
            if (ImageUtil.hasBackFacePicture(c)) {
                filename = ImageUtil.getImageKey(c, true, true) + ".jpg";
                cardFileNames.put(filename, filename);
            }
        }
    }

    Map<String, Map<String, String>> cardFileNamesBySet;
    Map<String, String> nameUpdates;

    private void analyzeCardPicsSetDir(final File root) {
        if (null == cardFileNamesBySet) {
            cardFileNamesBySet = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            for (final CardEdition ce : FModel.getMagicDb().getEditions()) {
                final Map<String, String> cardFileNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
                final Predicate<PaperCard> filter = IPaperCard.Predicates.printedInSet(ce.getCode());
                addSetCards(cardFileNames, FModel.getMagicDb().getCommonCards().getAllCards(), filter);
                addSetCards(cardFileNames, FModel.getMagicDb().getVariantCards().getAllCards(), filter);
                cardFileNamesBySet.put(ce.getCode2(), cardFileNames);
            }

            // planar cards now don't have the ".full" part in their filenames
            nameUpdates = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            final Predicate<PaperCard> predPlanes = new Predicate<PaperCard>() {
                @Override
                public boolean apply(final PaperCard arg0) {
                    return arg0.getRules().getType().isPlane() || arg0.getRules().getType().isPhenomenon();
                }
            };

            for (final PaperCard c : Iterables.filter(FModel.getMagicDb().getVariantCards().getAllCards(),
                    predPlanes)) {
                String baseName = ImageUtil.getImageKey(c, false, true);
                nameUpdates.put(baseName + ".full.jpg", baseName + ".jpg");
                if (ImageUtil.hasBackFacePicture(c)) {
                    baseName = ImageUtil.getImageKey(c, true, true);
                    nameUpdates.put(baseName + ".full.jpg", baseName + ".jpg");
                }
            }
        }

        final CardEdition.Collection editions = FModel.getMagicDb().getEditions();
        final String editionCode = root.getName();
        final CardEdition edition = editions.get(editionCode);
        if (null == edition) {
            // not a valid set name, skip
            numFilesAnalyzed += countFiles(root);
            return;
        }

        final String editionCode2 = edition.getCode2();
        final Map<String, String> validFilenames = cardFileNamesBySet.get(editionCode2);
        analyzeListedDir(root, ForgeConstants.CACHE_CARD_PICS_DIR, new ListedAnalyzer() {
            @Override
            public String map(String filename) {
                filename = editionCode2 + "/" + filename;
                if (nameUpdates.containsKey(filename)) {
                    filename = nameUpdates.get(filename);
                }
                if (validFilenames.containsKey(filename)) {
                    return validFilenames.get(filename);
                } else if (StringUtils.endsWithIgnoreCase(filename, ".jpg")
                        || StringUtils.endsWithIgnoreCase(filename, ".png")) {
                    return filename;
                }
                return null;
            }

            @Override
            public OpType getOpType(final String filename) {
                return validFilenames.containsKey(filename) ? OpType.SET_CARD_PIC : OpType.POSSIBLE_SET_CARD_PIC;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // other image dirs
    //

    Map<String, String> iconFileNames;

    private void analyzeIconsPicsDir(final File root) {
        if (null == iconFileNames) {
            iconFileNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            for (final Pair<String, String> nameurl : FileUtil
                    .readNameUrlFile(ForgeConstants.IMAGE_LIST_QUEST_OPPONENT_ICONS_FILE)) {
                iconFileNames.put(nameurl.getLeft(), nameurl.getLeft());
            }
        }

        analyzeListedDir(root, ForgeConstants.CACHE_ICON_PICS_DIR, new ListedAnalyzer() {
            @Override
            public String map(final String filename) {
                return iconFileNames.containsKey(filename) ? iconFileNames.get(filename) : null;
            }

            @Override
            public OpType getOpType(final String filename) {
                return OpType.QUEST_PIC;
            }
        });
    }

    Map<String, String> tokenFileNames;
    Map<String, String> questTokenFileNames;

    private void analyzeTokenPicsDir(final File root) {
        if (null == tokenFileNames) {
            tokenFileNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            questTokenFileNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            for (final Pair<String, String> nameurl : FileUtil
                    .readNameUrlFile(ForgeConstants.IMAGE_LIST_TOKENS_FILE)) {
                tokenFileNames.put(nameurl.getLeft(), nameurl.getLeft());
            }
            for (final Pair<String, String> nameurl : FileUtil
                    .readNameUrlFile(ForgeConstants.IMAGE_LIST_QUEST_TOKENS_FILE)) {
                questTokenFileNames.put(nameurl.getLeft(), nameurl.getLeft());
            }
        }

        analyzeListedDir(root, ForgeConstants.CACHE_TOKEN_PICS_DIR, new ListedAnalyzer() {
            @Override
            public String map(final String filename) {
                if (questTokenFileNames.containsKey(filename)) {
                    return questTokenFileNames.get(filename);
                }
                if (tokenFileNames.containsKey(filename)) {
                    return tokenFileNames.get(filename);
                }
                return null;
            }

            @Override
            public OpType getOpType(final String filename) {
                return questTokenFileNames.containsKey(filename) ? OpType.QUEST_PIC : OpType.TOKEN_PIC;
            }
        });
    }

    private void analyzeProductPicsDir(final File root) {
        // we don't care about the files in the root dir -- the new booster files are .png, not the current .jpg ones
        analyzeDir(root, new Analyzer() {
            @Override
            boolean onDir(final File dir) {
                final String dirName = dir.getName();
                if ("booster".equalsIgnoreCase(dirName)) {
                    analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_BOOSTERS_FILE,
                            ForgeConstants.CACHE_BOOSTER_PICS_DIR, OpType.QUEST_PIC);
                } else if ("fatpacks".equalsIgnoreCase(dirName)) {
                    analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_FATPACKS_FILE,
                            ForgeConstants.CACHE_FATPACK_PICS_DIR, OpType.QUEST_PIC);
                } else if ("boosterboxes".equalsIgnoreCase(dirName)) {
                    analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_BOOSTERBOXES_FILE,
                            ForgeConstants.CACHE_BOOSTERBOX_PICS_DIR, OpType.QUEST_PIC);
                } else if ("precons".equalsIgnoreCase(dirName)) {
                    analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_PRECONS_FILE,
                            ForgeConstants.CACHE_PRECON_PICS_DIR, OpType.QUEST_PIC);
                } else if ("tournamentpacks".equalsIgnoreCase(dirName)) {
                    analyzeSimpleListedDir(dir, ForgeConstants.IMAGE_LIST_QUEST_TOURNAMENTPACKS_FILE,
                            ForgeConstants.CACHE_TOURNAMENTPACK_PICS_DIR, OpType.QUEST_PIC);
                } else {
                    return false;
                }
                return true;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // preferences
    //

    private void analyzePreferencesDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                final String filename = file.getName();
                if ("editor.preferences".equalsIgnoreCase(filename)
                        || "forge.preferences".equalsIgnoreCase(filename)) {
                    final File targetFile = new File(ForgeConstants.USER_PREFS_DIR,
                            filename.toLowerCase(Locale.ENGLISH));
                    if (!file.equals(targetFile)) {
                        cb.addOp(OpType.PREFERENCE_FILE, file, targetFile);
                    }
                }
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // quest data
    //

    private void analyzeQuestDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                final String filename = file.getName();
                if ("all-prices.txt".equalsIgnoreCase(filename)) {
                    final File targetFile = new File(ForgeConstants.DB_DIR, filename.toLowerCase(Locale.ENGLISH));
                    if (!file.equals(targetFile)) {
                        cb.addOp(OpType.DB_FILE, file, targetFile);
                    }
                }
            }

            @Override
            boolean onDir(final File dir) {
                if ("data".equalsIgnoreCase(dir.getName())) {
                    analyzeQuestDataDir(dir);
                    return true;
                }
                return false;
            }
        });
    }

    private void analyzeQuestDataDir(final File root) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                final String filename = file.getName();
                if (StringUtils.endsWithIgnoreCase(filename, ".dat")) {
                    final File targetFile = new File(ForgeConstants.QUEST_SAVE_DIR, lcaseExt(filename));
                    if (!file.equals(targetFile)) {
                        cb.addOp(OpType.QUEST_DATA, file, targetFile);
                    }
                }
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    // utility functions
    //

    private class Analyzer {
        void onFile(final File file) {
        }

        // returns whether the directory has been handled
        boolean onDir(final File dir) {
            return false;
        }
    }

    private void analyzeDir(final File root, final Analyzer analyzer) {
        File[] files = root.listFiles();
        assert files != null;
        for (final File file : files) {
            if (cb.checkCancel()) {
                return;
            }

            if (file.isFile()) {
                ++numFilesAnalyzed;
                analyzer.onFile(file);
            } else if (file.isDirectory()) {
                if (!analyzer.onDir(file)) {
                    numFilesAnalyzed += countFiles(file);
                }
            }
        }
    }

    private final Map<String, Map<String, String>> fileNameDb = new HashMap<>();

    private void analyzeSimpleListedDir(final File root, final String listFile, final String targetDir,
            final OpType opType) {
        if (!fileNameDb.containsKey(listFile)) {
            final Map<String, String> fileNames = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
            for (final Pair<String, String> nameurl : FileUtil.readNameUrlFile(listFile)) {
                // we use a map instead of a set since we need to match case-insensitively but still map to the correct case
                fileNames.put(nameurl.getLeft(), nameurl.getLeft());
            }
            fileNameDb.put(listFile, fileNames);
        }

        final Map<String, String> fileDb = fileNameDb.get(listFile);
        analyzeListedDir(root, targetDir, new ListedAnalyzer() {
            @Override
            public String map(final String filename) {
                return fileDb.containsKey(filename) ? fileDb.get(filename) : null;
            }

            @Override
            public OpType getOpType(final String filename) {
                return opType;
            }
        });
    }

    private abstract class ListedAnalyzer {
        abstract String map(String filename);

        abstract OpType getOpType(String filename);

        // returns whether the directory has been handled
        boolean onDir(final File dir) {
            return false;
        }
    }

    private void analyzeListedDir(final File root, final String targetDir, final ListedAnalyzer listedAnalyzer) {
        analyzeDir(root, new Analyzer() {
            @Override
            void onFile(final File file) {
                final String filename = listedAnalyzer.map(file.getName());
                if (null != filename) {
                    final File targetFile = new File(targetDir, filename);
                    if (!file.equals(targetFile)) {
                        cb.addOp(listedAnalyzer.getOpType(filename), file, targetFile);
                    }
                }
            }

            @Override
            boolean onDir(final File dir) {
                return listedAnalyzer.onDir(dir);
            }
        });
    }

    private int countFiles(final File root) {
        int count = 0;
        File[] files = root.listFiles();
        assert files != null;
        for (final File file : files) {
            if (cb.checkCancel()) {
                return 0;
            }

            if (file.isFile()) {
                ++count;
            } else if (file.isDirectory()) {
                count += countFiles(file);
            }
        }
        return count;
    }

    private static String lcaseExt(final String filename) {
        final int lastDotIdx = filename.lastIndexOf('.');
        if (0 > lastDotIdx) {
            return filename;
        }
        final String basename = filename.substring(0, lastDotIdx);
        final String ext = filename.substring(lastDotIdx).toLowerCase(Locale.ENGLISH);
        if (filename.endsWith(ext)) {
            return filename;
        }
        return basename + ext;
    }
}