Java tutorial
/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package modmanager.backend; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.SwingWorker; import modmanager.ModificationManager; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; import util.Util; /** * * @author ruman */ public final class Modification { private static final Logger logger = Logger.getLogger(Modification.class.getName()); private static final IOFileFilter README_FILENAME_FILTER = new IOFileFilter() { @Override public boolean accept(File file, String filename) { filename = filename.toLowerCase(); if (FilenameUtils.wildcardMatch(filename, "*.txt") && !filename.equals("wizard.txt")) { return true; } return false; } @Override public boolean accept(File file) { return accept(file, file.getName()); } }; private static final IOFileFilter IMAGE_FILENAME_FILTER = new IOFileFilter() { @Override public boolean accept(File file, String filename) { if (FilenameUtils.wildcardMatch(filename, "*.png") || FilenameUtils.wildcardMatch(filename, "*.jpg") || FilenameUtils.wildcardMatch(filename, "*.bmp") || FilenameUtils.wildcardMatch(filename, "*.jpeg")) { return true; } return false; } @Override public boolean accept(File file) { return accept(file, file.getName()); } }; /** * The mod directory */ private File directory; /** * Determines if the mod could be loaded */ private boolean loaded; /** * The name, usually extracted from directory name */ private String name; /** * The modification type */ private Type type; /** * Contains all modification options */ private HashMap<String, ModificationOption> options; /** * Contains information if this modification is activated */ private ModificationStatus status; /** * Event listeners */ private ArrayList<ModificationListener> listeners; /** * Instance to mod manager */ private ModificationManager manager; public Modification(ModificationManager manager, File path) { this.manager = manager; options = new HashMap<>(); status = new ModificationStatus(this); listeners = new ArrayList<>(); load(path); updateStatus(); } private void load(File path) { logger.log(Level.INFO, "Loading modification from {0}", path.getAbsolutePath()); if (!path.isDirectory()) { return; } /** * Precheck: dir empty? (failed extraction) */ if (path.list().length == 0) { path.delete(); return; } /** * Precheck: archive ghost dir? */ { File[] entries = path.listFiles(); if (entries.length == 1) { File one = entries[0]; if (one.isDirectory()) { if (!one.getName().equalsIgnoreCase("data")) { try { logger.log(Level.INFO, "Moving directory, because someone decided it's good to pack all data into a sub directory ..."); /** * It's a directory. Change filesystem by * reparenting */ File tmp = new File(path.getAbsolutePath() + ".tmp"); FileUtils.moveDirectory(one, tmp); FileUtils.deleteDirectory(path); FileUtils.moveDirectory(tmp, path); } catch (IOException ex) { logger.log(Level.SEVERE, ex.getMessage()); return; } } } } } this.directory = path; /** * Read name */ this.name = path.getName(); /** * Wizard indicates */ if ((new File(path, "Wizard.txt")).exists()) { this.type = Type.MULTIOPTION; } else { this.type = Type.MULTIOPTION; for (File sub : path.listFiles()) { if (FilenameUtils.wildcardMatch(sub.getName(), "*.bsa", IOCase.INSENSITIVE)) { this.type = Type.SIMPLE; break; } else if (FilenameUtils.wildcardMatch(sub.getName(), "*.esp", IOCase.INSENSITIVE)) { this.type = Type.SIMPLE; break; } else if (FilenameUtils.wildcardMatch(sub.getName(), "Meshes", IOCase.INSENSITIVE)) { this.type = Type.SIMPLE; break; } else if (FilenameUtils.wildcardMatch(sub.getName(), "Textures", IOCase.INSENSITIVE)) { this.type = Type.SIMPLE; break; } else if (FilenameUtils.wildcardMatch(sub.getName(), "data", IOCase.INSENSITIVE)) { this.type = Type.SIMPLE; break; } } } logger.log(Level.INFO, "... mod type detected: {0}", this.type); /** * OK, load the mod types from given type */ switch (this.type) { /** * This is intended! */ case MULTIOPTION: { for (File sub : path.listFiles()) { if (sub.isDirectory() && !sub.getName().equalsIgnoreCase("fomod") && !sub.getName().toLowerCase().contains("readme") && !sub.getName().toLowerCase().contains("manual")) { ModificationOption opt = new ModificationOption(this, sub); if (opt.getFileCount() != 0) { options.put(opt.getName(), opt); } } } } break; case SIMPLE: { //Create new option based on modification path ModificationOption opt = new ModificationOption(this, path); if (opt.getFileCount() != 0) { options.put(opt.getName(), opt); } } break; } /** * Load screenshots */ for (File file : getScreenshotImages()) { status.addScreenshotImage(file); } if (!options.isEmpty()) { loaded = true; } else { logger.log(Level.WARNING, "Ignoring {0}: Empty!", name); } } /** * Tries to find read me files and returns the text * * @return */ public String getReadMe() { StringBuilder readme = new StringBuilder(); for (File file : FileUtils.listFiles(directory, README_FILENAME_FILTER, TrueFileFilter.INSTANCE)) { try { readme.append(FileUtils.readFileToString(file)); readme.append("\n---------\n"); } catch (IOException ex) { } } if (readme.length() == 0) { readme.append("No readme found."); } return readme.toString(); } /** * Tries to find some images (screenshots, ...) * * @return */ public ArrayList<File> getScreenshotImages() { ArrayList<File> files = new ArrayList<>(); files.addAll(FileUtils.listFiles(directory, IMAGE_FILENAME_FILTER, TrueFileFilter.INSTANCE)); return files; } /** * @return the loaded */ public boolean isLoaded() { return loaded; } /** * @return the name */ public String getName() { return name; } /** * @return the type */ public Type getType() { return type; } public void updateStatus() { for (ModificationOption opt : getOptions()) { opt.updateStatus(getStatus()); } } /** * Checks if all files are installed This won't work for FOMOD or WIZARD * * @return */ public boolean isCompletelyInstalled() { switch (type) { /** * Cannot decide if we used a wizard */ case MULTIOPTION: return isInstalled(); case SIMPLE: return getStatus().getOptionCount() == getStatus().getInstalledOptions(); } throw new IllegalStateException(); } /** * Checks if some files are installed * * @return */ public boolean isInstalled() { return getStatus().getInstalledOptions() != 0; } /** * @return the status */ public ModificationStatus getStatus() { return status; } /** * @return the options */ public Collection<ModificationOption> getOptions() { return options.values(); } /** * Installs this modifications with given options * * @param options */ public void install(ModificationOption... options) { if (isInstalled()) { uninstall(); } for (ModificationOption opt : options) { if (!this.options.values().contains(opt)) { throw new IllegalStateException("Option does not belong to this modification."); } opt.install(); } updateStatus(); for (ModificationListener lis : listeners) { lis.modificationInstalled(this, options); } } /** * Uninstalls this modification. */ public void uninstall() { for (ModificationOption opt : options.values()) { opt.uninstall(); } updateStatus(); for (ModificationListener lis : listeners) { lis.modificationUninstalled(this); } } public void addListener(ModificationListener listener) { if (!listeners.contains(listener)) { listeners.add(listener); } } public void removeListener(ModificationListener listener) { listeners.remove(listener); } public ModificationOption getOption(String name) { return options.get(name); } public ModificationOption getFirstOption() { for (ModificationOption opt : options.values()) { return opt; } return null; } /** * Returns all options, which conflicts with current selection of * to-be-installed options. * * @param modifications * @param to_be_added * @return */ public static LinkedList<ModificationOption> findConflicts(Collection<Modification> modifications, Collection<ModificationOption> to_be_added) { LinkedList<ModificationOption> conflicts = new LinkedList<>(); for (Modification mod : modifications) { for (ModificationOption opt : mod.getOptions()) { /** * option should be installed OR in the list of to-be-added */ if (mod.getStatus().isInstalled(opt) || to_be_added.contains(opt)) { /** * Check all mods in collection */ for (ModificationOption wannabe : to_be_added) { /** * Prevent self-checking */ if (wannabe == opt) { continue; } /** * Only care about uninstalled selected packages */ if (!wannabe.getParent().getStatus().isInstalled(wannabe)) { if (opt.conflicts(wannabe)) { conflicts.add(opt); } } } } } } return conflicts; } /** * @return the manager */ public ModificationManager getModificationManager() { return manager; } /** * Returns directory of this modification * * @return */ public File getDirectory() { return directory; } public static enum Type { /** * Just some files */ SIMPLE, /** * Contains multiple options */ MULTIOPTION } /** * * @author ruman */ public static class ModificationUninstaller extends SwingWorker<Modification, String> { private Modification modification; public ModificationUninstaller(Modification mod) { this.modification = mod; } @Override protected Modification doInBackground() throws Exception { /** * Calculate size */ float count = 0; float copied = 0; for (ModificationOption option : modification.getOptions()) { count += option.getFileCount(); } for (ModificationOption option : modification.getOptions()) { publish(option.getName()); for (File file : option.getFiles()) { if (copied % 5 == 0) { setProgress((int) ((float) copied / (float) count * 100f)); } option.uninstall(file); copied++; } option.enforceFullStatusUpdate(); } modification.updateStatus(); return modification; } @Override protected void done() { for (ModificationListener lis : modification.listeners) { lis.modificationUninstalled(modification); } } } /** * * @author ruman */ public static class ModificationInstaller extends SwingWorker<Modification, String> { private Modification modification; private ModificationOption[] options; public ModificationInstaller(Modification mod, ModificationOption... options) { this.modification = mod; this.options = options; } @Override protected Modification doInBackground() throws Exception { /** * Pre: uninstall */ publish("Uninstalling existing files ..."); modification.uninstall(); /** * Calculate size */ float count = 0; float copied = 0; for (ModificationOption option : options) { count += option.getFileCount(); } for (ModificationOption option : options) { if (!modification.options.values().contains(option)) { throw new IllegalArgumentException(); } publish(option.getName()); for (File file : option.getFiles()) { if (copied % 5 == 0) { setProgress((int) ((float) copied / (float) count * 100f)); } option.install(file); copied++; } option.enforceFullStatusUpdate(); } modification.updateStatus(); return modification; } @Override protected void done() { for (ModificationListener lis : modification.listeners) { lis.modificationInstalled(modification, options); } } } /** * Gets the actual destination file in Skyrim directory for a source file * * @param absolute source * @return */ public String getRelativePathInModification(File absolute) { return Util.relativePath(directory, absolute); } }