net.flamefeed.ftb.modpackupdater.FileOperator.java Source code

Java tutorial

Introduction

Here is the source code for net.flamefeed.ftb.modpackupdater.FileOperator.java

Source

/*
 * 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 net.flamefeed.ftb.modpackupdater;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JOptionPane;
import static org.apache.commons.io.FileUtils.iterateFiles;
import org.apache.commons.io.filefilter.TrueFileFilter;

/**
 * This class contains file utility methods to support the rest of the application.
 * For example it can find the Minecraft installation directory.
 * 
 * @author Francis
 */

public class FileOperator {

    /*
     * Constants
     */

    // These are used while referencing the remoteHashes array
    private final int MD5HASH = 0;
    private final int FILENAME = 1;

    // Root of all remote downloads
    public final String REMOTE_FILES_LOCATION = "http://ftb.flamefeed.net/files";

    // Name of the hashfile on the remote server
    private final String HASHFILE_NAME = "hashfile.txt";

    /*
     * Member variables
     */

    /*
     * These String array lists hold the relative paths of all files which should
     * be downloaded or deleted.
     */

    private List<String> downloadQueue;
    private List<String> deleteQueue;

    /*
     * 2d array for file names and their respective hashes. Holds data extracted
     * from the remote hash-file.
     * 
     * This data includes all the file names which should be present inside the Minecraft
     * directory. MD5 hashes are included for each file in order to verify that
     * each local file matches its copy on the remote server. This is for files
     * with identical file names but potentially different versions.
     * 
     * A 2d array was chosen so that each file and hash could be referenced by
     * the same index. The inner array has 2 elements (filename and hash) while
     * the outer array has n elements, where n is the number of files on the remote
     * server.
     * 
     * File names are stored as relative paths, for example:
     * "mods/gregtechmod.zip" 
     */

    private String[][] remoteHashes;

    // Local file path to the vanilla Minecraft folder
    private String pathMinecraft;

    /*
     * Public class methods
     */

    /**
     * Constructor will perform tasks to initialise class member variables, such as
     * locating the local Minecraft folder, also downloading and parsing the hash-file.
     */

    public FileOperator() {
        computePathMinecraft();

        try {
            parseHashfile();
        } catch (IOException ex) {
            System.err.println("Exception caught while parsing hashfile");
            System.err.println(ex.getMessage());

            JOptionPane.showMessageDialog(null,
                    "Updater was unable to download and "
                            + "parse the remote hash-file.\nThis is probably because you are "
                            + "not connected to the internet,\nor the remote file server is down. "
                            + "Contact Sirloin if problems persist.");

            System.exit(0);
        }

        try {
            compareLocalFiles();
        } catch (IOException | NoSuchAlgorithmException ex) {
            System.err.println("Exception caught while comparing local files");
            System.err.println(ex.getMessage());

            JOptionPane.showMessageDialog(null,
                    "Updater had difficulties reading "
                            + "certain files on your\ncomputer. Ensure that this program has "
                            + "permission to read your\napplication data folder.");

            System.exit(0);
        }

        // Initialisation complete!
    }

    /**
     * Get an iterator that iterates through all entries on the download queue.
     * 
     * @return 
     * The ListIterator<String> which iterates through the download queue.
     */

    public ListIterator<String> getDownloadQueueIterator() {
        return downloadQueue.listIterator();
    }

    /**
     * Get an iterator that iterates through all entries on the delete queue.
     * 
     * @return 
     * The ListIterator<String> which iterates through the delete queue.
     */

    public ListIterator<String> getDeleteQueueIterator() {
        return deleteQueue.listIterator();
    }

    /**
     * Obtains the Minecraft installation path from the FileOperator member
     * variable.
     * 
     * @return 
     * The Minecraft installation path
     */

    public String getMinecraftPath() {
        return pathMinecraft;
    }

    /**
     * Remove an element from the download queue
     * 
     * @param element 
     * The contents of the String element to remove.
     */

    public void removeFromDownloadQueue(String element) {
        downloadQueue.remove(element);
    }

    /**
     * Remove an element from the delete queue
     * 
     * @param element 
     * The contents of the String element to remove.
     */

    public void removeFromDeleteQueue(String element) {
        deleteQueue.remove(element);
    }

    /**
     * Make sure all higher level directories are created before attempting to write to
     * a file.
     * 
     * @param path
     * Path must be the relative path of the file, for example:
     * "mods/gregtechmod.zip"
     */

    public void createDirectories(String path) {
        // Get absolute path
        path = pathMinecraft + "/" + path;
        // Find the directory in which this file is located
        String bottomDirectory = new File(path).getParent();
        // Create this bottom directory and any necessary parent directories
        // which may not yet exist
        new File(bottomDirectory).mkdirs();
    }

    /**
     * Download remote file and save output to file system. Called from constructor,
     * so final to prevent being overridden.
     * 
     * @param path
     * A relative path to the file which will be downloaded. This determines both
     * the target URL and the local destination path. Example:
     * "mods/gregtechmod.zip"
     * 
     * @throws java.io.IOException
     */

    public void downloadFile(String path) throws IOException {
        URL urlRemoteTarget = new URL(REMOTE_FILES_LOCATION + "/" + path);
        ReadableByteChannel in = Channels.newChannel(urlRemoteTarget.openStream());
        FileOutputStream out = new FileOutputStream(pathMinecraft + "/" + path);
        out.getChannel().transferFrom(in, 0, Long.MAX_VALUE);
    }

    /**
     * Goes through every file in the deleteQueue and removes it from the file system
     */

    public void executeDeleteQueue() {
        ListIterator<String> deleteQueueIterator = deleteQueue.listIterator();

        while (deleteQueueIterator.hasNext()) {
            new File(pathMinecraft + "/" + deleteQueueIterator.next()).delete();
        }
    }

    /*
     * Private class methods
     */

    /**
     * Obtain the absolute file path for the vanilla Minecraft directory based
     * on which Operating System is present
     */

    private void computePathMinecraft() {
        final String operatingSystem = System.getProperty("os.name");

        if (operatingSystem.contains("Windows") || operatingSystem.equals("Linux")) {
            pathMinecraft = System.getenv("appdata") + "/.minecraft";
        } else if (operatingSystem.contains("Mac")) {
            pathMinecraft = System.getenv("appdata") + "/minecraft";
        } else {
            pathMinecraft = System.getenv("appdata") + "/.minecraft";
            pathMinecraft = JOptionPane.showInputDialog(null,
                    "Operating system not recognised, please indicate installation path for vanilla Minecraft launcher.",
                    "Confirm Install Path", JOptionPane.PLAIN_MESSAGE, null, null, pathMinecraft).toString();
        }

        // Replace all \\ which might occur in Windows paths with / to make
        // it uniform. Sadly this requires a regex of \\, which as a string is \\\\.
        // Dear god.
        pathMinecraft = pathMinecraft.replaceAll("\\\\", "/");

        // Check validity of Minecraft directory
        File fileMinecraft = new File(pathMinecraft);

        if (pathMinecraft.equals("") || (fileMinecraft.exists() && !fileMinecraft.isDirectory())) {
            System.err.println("Invalid Minecraft installation path");
            System.err.println(pathMinecraft);
            JOptionPane.showMessageDialog(null,
                    "Minecraft installation path is invalid. Installer will now terminate.");
            System.exit(0);
        }

        // Make sure Minecraft directory is present. Does nothing if already there.
        fileMinecraft.mkdir();
    }

    /**
     * This method will download the hash-file from the server and
     * will extract data from it and populate the remoteHashes 2d array.
     *
     * @throws java.lang.IOException
     */

    private void parseHashfile() throws IOException {
        int numRemoteFiles;
        String[] currentLine;
        FileReader fr;
        BufferedReader br;
        LineNumberReader lnr;

        downloadFile(HASHFILE_NAME);

        /*
         * Compute the number of lines in the hash-file. This corresponds to the
         * number of files on the remote server, and therefore the length of the 
         * hashFiles outer array.
         */

        fr = new FileReader(pathMinecraft + "/" + HASHFILE_NAME);
        lnr = new LineNumberReader(fr);
        lnr.skip(Long.MAX_VALUE);
        numRemoteFiles = lnr.getLineNumber();
        fr.close();

        /*
         * Populate the remoteHashes[][] array with data from the hash file
         */

        remoteHashes = new String[numRemoteFiles][2];

        fr = new FileReader(pathMinecraft + "/" + HASHFILE_NAME);
        br = new BufferedReader(fr);

        /*
         * The hash-file format is as follows:
         * <MD5 hash><tab character><relative filename><newline character>
         * Splitting each line using a tab character "\t" separates the filename
         * from the MD5 hash.
         */

        for (int i = 0; i < numRemoteFiles; i++) {
            currentLine = br.readLine().split("\t", 2);
            remoteHashes[i][MD5HASH] = currentLine[MD5HASH];
            remoteHashes[i][FILENAME] = currentLine[FILENAME];
        }

        fr.close();
    }

    /**
     * Generates an MD5 checksum of a file in hexadecimal form
     * 
     * @param path
     * Relative path to the file which will be checked, for example:
     * "mods/gregtechmod.zip"
     * 
     * @return
     * The resulting hexadecimal checksum as a string.
     */

    private String getMD5Checksum(String path) throws IOException, NoSuchAlgorithmException {
        // MessageDigest provides the functionality of the MD5 algorithm
        MessageDigest MD5Digest;
        FileInputStream fis;
        BufferedInputStream bis;
        byte[] data = new byte[1024];
        int bytesRead;

        MD5Digest = MessageDigest.getInstance("MD5");

        fis = new FileInputStream(pathMinecraft + "/" + path);
        bis = new BufferedInputStream(fis, 1024);

        do {
            bytesRead = bis.read(data, 0, 1024);

            if (bytesRead > 0) {
                MD5Digest.update(data, 0, bytesRead);
            }
            // BufferedInputStream.read() returns -1 at end of stream.
        } while (bytesRead != -1);

        // Present MD5 hash as a String
        data = MD5Digest.digest();
        String checksum = "";
        for (byte b : data) {
            // Prime example of write-only code
            checksum += Integer.toString((b & 255) + 256, 16).substring(1);
        }

        return checksum;
    }

    /**
     * This method is used to compare local files against the server files.
     * Missing / outdated files are marked for download and excess files are
     * marked for deletion.
     * 
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */

    private void compareLocalFiles() throws IOException, NoSuchAlgorithmException {
        downloadQueue = new ArrayList<>();
        deleteQueue = new ArrayList<>();

        /*
         * First check for files which are on the hash-list, but are not on the
         * local file system. Also check for local files whose MD5 hash does not
         * match the hash indicated in the hash-file.
         */

        for (String[] remoteHash : remoteHashes) {
            File currentFile = new File(pathMinecraft + "/" + remoteHash[FILENAME]);

            if (currentFile.exists()) {
                String localHash = getMD5Checksum(remoteHash[FILENAME]);
                if (!localHash.equals(remoteHash[MD5HASH])) {
                    downloadQueue.add(remoteHash[FILENAME]);
                }
            } else {
                downloadQueue.add(remoteHash[FILENAME]);
            }
        }

        /*
         * Next check for files which are present on the local file system, but are
         * not present in the hash-file.
         */

        findUnaccountedFiles("mods");
        findUnaccountedFiles("config");
    }

    /**
     * Recursively search for a directory and locate any files which aren't accounted
     * for on the hash-file. Add these files to the deleteQueue.
     * 
     * @param path
     * The relative path to search for unaccounted files.
     * 
     * @throws IOException 
     */

    private void findUnaccountedFiles(String path) throws IOException {
        File pathToSearch = new File(pathMinecraft + "/" + path);

        if (pathToSearch.exists() && pathToSearch.isDirectory()) {
            /* 
             * This iterator is provided by Apache commons.io and will recurse
             * over all files in given directory and also its subdirectories.
             */

            Iterator<File> fileList = iterateFiles(pathToSearch, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE);

            while (fileList.hasNext()) {
                File currentFile = fileList.next();

                // Get the relative path of the file
                String relativePath = currentFile.getAbsolutePath();

                // Replace all \\ which might occur in Windows paths with / to make
                // it uniform. Sadly this requires a regex of \\, which as a string is \\\\.
                // Dear god.
                relativePath = relativePath.replaceAll("\\\\", "/");
                relativePath = relativePath.replaceFirst(pathMinecraft + "/", "");

                boolean fileAccountedFor = false;

                for (String[] remoteHash : remoteHashes) {
                    if (remoteHash[FILENAME].equals(relativePath)) {
                        fileAccountedFor = true;
                        break;
                    }
                }

                if (!fileAccountedFor) {
                    deleteQueue.add(relativePath);
                }
            }
        }
    }
}