Java Quiz Player

A Copy Files App using JavaFX - Java Source Code

FileTreeView.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.TreeView;
import javafx.scene.control.TreeItem;
import javafx.scene.control.Button;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tooltip;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.control.CheckBoxTreeItem.TreeModificationEvent;
import javafx.stage.DirectoryChooser;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.collections.ObservableList;
import javafx.application.Platform;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.logging.Logger;


/*
 * Main GUI class for this app.
 * Builds a tree view of the files for the selected file system root
 * directory. Has functions to expand, collapse the file tree and check
 * the files and directories for copy and open the copy files dialog.
 */
public class FileTreeView {

	
    private TreeView<Path> tree;
    private Button copyBtn;
    
    private CopyDialog copyDialog;
    private Path rootDir; // The chosen root or source directory
    
    // All file and directories that are checked are stored here
    private Set<Path> checkedItems;
    
    // Indecies of selected directory nodes, used with expand or collapse
    // of specific directory nodes.
    private List<Integer> selectedIndecies;
    
    // Flag to indicate if the root directory node is in expanded or
    // collapsed state. This is used with the tree expand/collapse toggle
    // button. In case the node is collapsed it is expanded and vice-versa.
    private boolean isExpanded;
    
    private static final String DEFAULT_DIRECTORY =
            System.getProperty("user.dir"); //  or "user.home"
    private static Logger logger;


    /*
     * Constructor builds and displays the app's main view.
     */
    public FileTreeView(Stage primaryStage) {

        logger = Logger.getLogger("copy_app_logger");
        copyDialog = new CopyDialog();
        checkedItems = new HashSet<Path>();
        
        Button expandBtn = new Button('\u2039' + " " + '\u203A');
        expandBtn.setTooltip(new Tooltip("Expand or collapse selected tree items"));
        expandBtn.setOnAction(e -> expandOrCollapseSelectedItemsRoutine());
        Button expandAllBtn = new Button('\u00AB' + " " + '\u00BB');
        expandAllBtn.setTooltip(new Tooltip("Expand or collapse entire tree"));
        expandAllBtn.setOnAction(e -> expandOrCollapseTreeRoutine());
        
        HBox hb1 = new HBox(15);
        hb1.setAlignment(Pos.CENTER);
        hb1.getChildren().addAll(expandAllBtn, expandBtn);

        Button sourceDirBtn = new Button("Source directory...");
        sourceDirBtn.setTooltip(new Tooltip("Set a new root directory"));
        sourceDirBtn.setOnAction(e -> {
                chooseSourceDirectory(primaryStage);
                tree.setRoot(getRootItem());
        });
        copyBtn = new Button("Copy dialog...");
        copyBtn.setTooltip(new Tooltip("Initiate the copy action: opens copy dialog"));
        copyBtn.setDisable(true);
        copyBtn.setOnAction(e -> copyDialogRoutine());
        
        HBox hb2 = new HBox(15);
        hb2.setAlignment(Pos.CENTER);
        hb2.getChildren().addAll(sourceDirBtn, copyBtn);
        
        VBox vb = new VBox(20);
        vb.setPadding(new Insets(20));
        
        primaryStage.setScene(new Scene(vb, 800, 600)); // w, h
        primaryStage.setTitle("Copy Files App");
        primaryStage.show();
        
        chooseSourceDirectory(primaryStage);
        vb.getChildren().addAll(buildFileTreeView(), hb1, hb2);
    }
    
    /*
     * Opens the directory chooser with an initial root directory.
     * Allows the user to use the same, or select another one.
     * In case the chooser is cancelled a root directory is derived.
     */
    private void chooseSourceDirectory(Stage primaryStage) {
    
        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select a source directory");
        chooser.setInitialDirectory(getInitialDirectory().toFile());
        File chosenDir = chooser.showDialog(primaryStage);
        determineRootDirectory(chosenDir);
        copyBtn.setDisable(true);
        checkedItems.clear();
        isExpanded = false;
        logger.info("Root dir chosen: " + rootDir);
    }

    /*
     * Initial root directory set for the directory chooser.
     */
    private Path getInitialDirectory() {

        return (rootDir == null) ? Paths.get(DEFAULT_DIRECTORY) : rootDir;
    }
    
    /*
     * The root directory for the tree is derived here.
     * If a directory is chosen it is the root directory.
     * If not, the previous root directory is used. In case of no
     * previous directory the DEFAULT_DIRECTORY is the root.
     */
    private void determineRootDirectory(File chosenDir) {
        
        rootDir = (chosenDir != null) ?
                        chosenDir.toPath() : getInitialDirectory();
    }
    
    /*
     * Creates and returns the root item for the root directory.
     */
    private FileTreeItem getRootItem() {
    
        FileTreeItem rootItem = new FileTreeItem(rootDir);
        rootItem.setIndependent(false);
        rootItem.addEventHandler(
            CheckBoxTreeItem.<Path>checkBoxSelectionChangedEvent(),    
                (TreeModificationEvent<Path> e) -> handleItemCheckedEvent(e));
        return rootItem;
    }

    /*
     * Builds and returns the tree view for the given root item.
     */
    private TreeView buildFileTreeView() {
        
        tree = new TreeView<Path>(getRootItem());
        tree.setPrefHeight(600.0d);
        tree.setTooltip(new Tooltip("Expand (all) or collapse the tree, select items and copy..."));
        tree.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        tree.setCellFactory((TreeView<Path> t) ->  new TreeCellImpl());
        return tree;
    }
    
    /*
     * Event handler for the tree item checked or unchecked event.
     * On checking or unchecking an item the related items are stored
     * or removed from a Set collection respectively. These items in 
     * the collection are used for copying.
     */
    private void handleItemCheckedEvent(TreeModificationEvent<Path> e) {
    
        FileTreeItem item = (FileTreeItem) e.getTreeItem();
            
        if (item.isSelected() || item.isIndeterminate()) {
            
            collectCheckedItems(item);
        }
        else {
            removeCollectedCheckedItems(item);
        }
        
        copyBtn.setDisable((checkedItems.isEmpty()) ? true : false);
    }

    private void collectCheckedItems(FileTreeItem item) {

        if (item.isSelected() || item.isIndeterminate()) {

            // A file or directory is checked, or if a directory
            // with files has some items checked: collect these items
            checkedItems.add(item.getValue());
        }
        
        item.getChildren().forEach(t -> collectCheckedItems((FileTreeItem) t));
    }
    
    private void removeCollectedCheckedItems(FileTreeItem item) {

        if (! item.isSelected()) {

            // A file or directory is unchecked: remove these items
            checkedItems.remove(item.getValue());
        }
        
        item.getChildren()
                .forEach(t -> removeCollectedCheckedItems((FileTreeItem) t));
    }

    /*
     * Expand or collapse the entire file tree.
     */
    private void expandOrCollapseTreeRoutine() {
    
        FileTreeItem rootItem = (FileTreeItem) tree.getRoot();
        expandOrCollapseTree(rootItem);        
        isExpanded = (isExpanded) ? false : true;
    }
    
    private void expandOrCollapseTree(FileTreeItem item) {

        if (! item.isLeaf()) { 
        
            item.setExpanded((isExpanded) ? false : true);    
            item.getChildren().forEach(t -> expandOrCollapseTree((FileTreeItem) t));
        }
    }

    /*
     * Expand or collapse the selected items (directories) in the file tree.
     * NOTE: this is not the check box select, it is the item selection.
     * An item is highlighted when it is selected. Multiple items can
     * be selected at a time.
     */
    private void expandOrCollapseSelectedItemsRoutine() {

        selectedIndecies = new ArrayList<Integer>();
        ObservableList<TreeItem<Path>> items =
                tree.getSelectionModel().getSelectedItems();
        items.forEach(t -> expandOrCollapseSelectedItems((FileTreeItem) t));
        setSelectedItems();
    }

    private void expandOrCollapseSelectedItems(FileTreeItem item) {

        if ((item != null) && (! item.isLeaf())) {

            selectedIndecies.add(tree.getRow(item));
            item.setExpanded((item.isExpanded()) ? false : true);    
            item.getChildren()
                    .forEach(t -> expandOrCollapseSelectedItems((FileTreeItem) t));
        }
    }
    
    /*
     * Select the items (retain the selection) which were in selected
     * state before the items expand or collapse action.
     */
    private void setSelectedItems() {
        
        if (selectedIndecies.isEmpty()) {
        
            return; // there is no selection, can't expand or collapse
        }
        
        // One or more items selected
        
        int firstIndex = selectedIndecies.get(0);
        int [] remaining = new int [selectedIndecies.size() - 1];
        
        for (int j = 0, i = 1; j < remaining.length; j++) {
        
            remaining [j] = selectedIndecies.get(i);
            i++;
        }

        tree.getSelectionModel().selectIndices(firstIndex, remaining);
    }
    
    /*
     * Copy dialog button action routine.
     * Opens the CopyDialog modal dialog.
     */
    private void copyDialogRoutine() {
    
        logger.info("Copy dialog...");
    
        FileTreeItem rootItem = (FileTreeItem) tree.getRoot();
        Path sourceDir = rootItem.getValue();
        copyDialog.create(sourceDir, checkedItems);
    }

    /*
     * Inner class to render check boxes with file names for the tree items.
     */
    private class TreeCellImpl extends CheckBoxTreeCell<Path> {

        @Override
        public void updateItem(Path path, boolean empty) {
        
            super.updateItem(path, empty);
 
            if (empty) {
        
                setText(null);
            }
            else {
                if (path != null) {
        
                    String s = path.getFileName().toString();
                    setText(s);
                }
            }
        }
    }
}

FileTreeItem.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.scene.control.TreeItem;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import java.util.stream.Collectors;


/*
 * This class is a TreeItem for the given file Path with a checkbox. It does
 * this by overriding the TreeItem's getChildren() and the isLeaf() methods.
 * Note that CheckBoxTreeItem extends TreeItem.
 */
public class FileTreeItem extends CheckBoxTreeItem<Path> {


    // Cache whether the file is a leaf or not. A file is a leaf if
    // it is not a directory. The isLeaf() is called often, and doing
    // the actual check on Path is expensive.
    private boolean isLeaf;
 
    // Do the children and leaf testing only once, and then set these
    // booleans to false so that we do not check again during this run.
    private boolean isFirstTimeChildren = true;
    private boolean isFirstTimeLeaf = true;


    /*
     * Constructor.
     * The parameter is the root or source input directory path used
     * to build the file tree with the TreeView control.
     */
    public FileTreeItem(Path path) {
    
        super(path);
    }

    @Override
    public boolean isLeaf() {

        if (isFirstTimeLeaf) {

            isFirstTimeLeaf = false;
            Path path = getValue();
            isLeaf = Files.isRegularFile(path);
        }

        return isLeaf;
    }

    /*
     * Returns a list that contains the child TreeItems belonging to the TreeItem.
     */
    @Override
    public ObservableList<TreeItem<Path>> getChildren() {

        if (isFirstTimeChildren) {

            isFirstTimeChildren = false;

            // First getChildren() call, so we actually go off and 
            // determine the children of the file contained in this TreeItem.
            super.getChildren().setAll(buildChildren(this));
        }

        return super.getChildren();
    }

    private ObservableList<TreeItem<Path>> buildChildren(
            CheckBoxTreeItem<Path> treeItem) {

        Path path = treeItem.getValue();        

        if ((path != null) && (Files.isDirectory(path))) {

            try(Stream<Path> pathStream = Files.list(path)) {

                return pathStream
                        .map(p -> new FileTreeItem(p))
                            .collect(Collectors.toCollection(() ->
                                    FXCollections.observableArrayList()));
            }
            catch(IOException e) {

                throw new UncheckedIOException(e);
            }
        }

        return FXCollections.emptyObservableList();
    }
}

CopyDialog.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextArea;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tooltip;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ProgressBar;
import javafx.scene.Scene;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.Modality;
import javafx.stage.DirectoryChooser;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.application.Platform;

import java.util.Set;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.attribute.BasicFileAttributes;


/*
 * Constructs the copy files dialog. This is invoked from the app's main GUI.
 * This dialog captures the info to perform the copy of the selected files
 * in the main view: the target directory, the file filters and an option to
 * create a ZIP file. Starts the copy process: the file filters are applied
 * to the selected files and the filtered files are copied to the target. The 
 * process can be cancelled if needed. The process progress is viewed thru a
 * progress bar and the status is logged to the status area.
 */
public class CopyDialog {


    private TextArea statusArea;
    private Button selectTargetBtn;
    private Button filtersBtn;
    private Button copyBtn;
    private Button cancelBtn;
    private Button closeBtn;
    private CheckBox zipCheckBox;
    private ProgressBar progressBar;

    private FileFilterDialog fileFiltersDialog;
    private FileFilters fileFilters;
    
    // The root or the source directory input from the main GUI with the
    // file tree view. 
    private Path sourceDir;
    
    // The target or destination directory to which the files are copied to.
    // This is obtained from a directory chooser in this dialog.
    private Path targetDir;

    // The files copy is performed in a background thread by this Task object.
    // See copyRoutine() method.
    private Task<Void> copyTask;
    
    // Counters for total files and directories that are actually copied.
    // These are used to show the status after the copy task is complete.
    private int copiedFilesCount;
    private int copiedDirsCount;


    private static final String DEFAULT_DIRECTORY =
            System.getProperty("user.dir"); //  or "user.home"
    private static Logger logger;
            
            
    /*
     * Constructor.
     * Creates a copy of the FileFilterDialog.
     * Obtains the logger and configures it with the TextAreaLogHandler.
     */
    public CopyDialog() {
    
        logger = Logger.getLogger("copy_app_logger");
        statusArea = getTextArea();
        TextAreaLogHandler handler = new TextAreaLogHandler(statusArea);
        logger.addHandler(handler);
        fileFiltersDialog = new FileFilterDialog();
    }

    /*
     * Constructs the GUI for the Copy dialog.
     */
    public void create(Path sourceDir, Set<Path> selectedFiles) {
        
        this.sourceDir = sourceDir;
        
        Stage dialog = new Stage();
        dialog.setResizable(false);
        dialog.setTitle("Copy Files");
        dialog.initModality(Modality.APPLICATION_MODAL);
        dialog.setOnCloseRequest(e -> e.consume()); // disable Close (x) button
        
        Platform.runLater(() -> statusArea.setText(""));
        
        selectTargetBtn = new Button("Target directory...");
        selectTargetBtn.setTooltip(new Tooltip("Select a target directory"));
        selectTargetBtn.setOnAction(e -> chooseTargetDirectory());
        filtersBtn = new Button("Filters...");
        filtersBtn.setTooltip(new Tooltip("Apply file filters"));
        filtersBtn.setOnAction(e -> getFilters());
        filtersBtn.setDisable(true);
        zipCheckBox = new CheckBox("Create ZIP file");
        zipCheckBox.setDisable(true);
        copyBtn = new Button("Copy files...");
        copyBtn.setTooltip(new Tooltip("Copy files to target directory"));
        copyBtn.setOnAction(e -> copyRoutine(selectedFiles));
        copyBtn.setDisable(true);
        cancelBtn = new Button("Cancel copy");
        cancelBtn.setTooltip(new Tooltip("Cancel or abort the copy process"));
        cancelBtn.setOnAction(e -> {
            if (copyTask != null) {
                copyTask.cancel();
            }
        });
        cancelBtn.setDisable(true);
        closeBtn = new Button("Close");
        closeBtn.setTooltip(new Tooltip("Close the dialog"));
        closeBtn.setOnAction(e -> dialog.close());
        
        HBox btnHb = new HBox(15);
        btnHb.setAlignment(Pos.CENTER);
        
        /*
         * The ZIP file create option checkbox is available with the
         * Windows operating system only.
         */
        if (System.getProperty("os.name").toLowerCase().contains("windows")) {

            btnHb.getChildren().addAll(selectTargetBtn, filtersBtn, zipCheckBox, copyBtn, cancelBtn, closeBtn);
        }
        else {
            btnHb.getChildren().addAll(selectTargetBtn, filtersBtn, copyBtn, cancelBtn, closeBtn);
        }

        progressBar = new ProgressBar();
        progressBar.setPrefWidth(600.0d);
        progressBar.setTooltip(new Tooltip("Copy files process progress"));
        
        HBox statusHb = new HBox();
        statusHb.setAlignment(Pos.CENTER);
        statusHb.getChildren().addAll(progressBar);
        
        VBox vb = new VBox(20);
        vb.setPadding(new Insets(15, 15, 5, 15));
        vb.getChildren().addAll(statusArea, statusHb, btnHb);

        dialog.setScene(new Scene(vb));

        String initialText =
                "* Copy Files to a Target Directory * \n" +
                "Total directories (includes root) and files selected: " +
                selectedFiles.size() + " " +
                "\nSource directory: " + sourceDir.toString() + " " +
                "\n\nSelect a target directory, apply file filters and copy.\n";
        logger.info(initialText);

        dialog.showAndWait(); // this shows a modal window
    }
    
    private TextArea getTextArea() {
    
        TextArea textArea = new TextArea();
        textArea.setTooltip(new Tooltip("Status message area"));
        textArea.setPrefRowCount(14);
        textArea.setPrefColumnCount(60);
        textArea.setEditable(false);
        textArea.setFont(new Font("Verdana", 16));
        return textArea;
    }
    
    /*
     * Opens the directory chooser and lets the user select a
     * target directory for copying the selected files. The
     * directory is verified if it is valid.
     */
    private void chooseTargetDirectory() {

        DirectoryChooser chooser = new DirectoryChooser();
        chooser.setTitle("Select a target directory");
        chooser.setInitialDirectory(new File(DEFAULT_DIRECTORY));
        File chosenDir = chooser.showDialog(null);
        
        targetDir = (chosenDir == null) ? null : chosenDir.toPath();
        
        if (! verifyDirectories()) {
        
            return;
        }

        logger.info("Target directory: " + targetDir.toString());
        progressBar.progressProperty().unbind();
        progressBar.setProgress(0);
        fileFilters = null;
        zipCheckBox.setDisable(false);
        copyBtn.setDisable(false);
        filtersBtn.setDisable(false);
        filtersBtn.requestFocus();
    }
    
    /*
     * Checks if the target directory path is not the same as
     * that of the source path, or the target is not within the 
     * source directory structure; shows an alert message.
     */
    private boolean verifyDirectories() {
        
        if (targetDir == null) {

            showAlertDialog("No directory selected!");
            return false;
        }
        
        if ((sourceDir.equals(targetDir)) ||
                (targetDir.startsWith(sourceDir))) {
        
            showAlertDialog("Source and target directories are same, or " +
                            "the target is within the source.");
            return false;
        }

        if (targetDir.toFile().list().length > 0) {
        
            logger.warning("The target directory is not empty.");
        }
    
        return true;
    }
    
    /*
     * Displays a modal alert with the supplied message.
     */
    private void showAlertDialog(String msg) {
    
        Alert alert = new Alert(AlertType.NONE);
        alert.setTitle("Files Copy");
        alert.getDialogPane().getButtonTypes().add(ButtonType.OK);
        alert.setContentText(msg);
        alert.show();
    }
    
    /*
     * Displays the file filters dialog and captures user input. Gets
     * the selected file filter options as an instance of FileFilters.
     */
    private void getFilters() {
        
        Dialog<FileFilters> dialog = fileFiltersDialog.create();        
        Optional<FileFilters> result = dialog.showAndWait();

        if (result.isPresent()) {
        
            fileFilters = result.get();
            
            if (fileFilters.getFileTypes().isEmpty()) {
            
                // In case there is no selection in the file types list,
                // which is possible with a ListView, the value is set.
                fileFilters.setFileTypes(FXCollections.observableArrayList("All"));
            }
        }

        logger.info("File filters: " + fileFilters.toString());
        zipCheckBox.requestFocus();
    }

    /*
     * Routine for the Copy files button action.
     * 1. Applies the file filters to the selected files.
     * 2. Copies the filtered files to target directory.
     * 3. Creates a ZIP file if the option is selected.
     * These tasks are performed as a JavaFX concurrent Task. At end,
     * a status (Succeeded, Failed/exception or Cancelled) is displayed
     * in the status message area.
     */
    private void copyRoutine(Set<Path> inputSelectedFiles) {

        copiedFilesCount = 0;
        copiedDirsCount = 0;
        
        copyTask = new Task<Void>() {

            int currentCounter;
        
            @Override
            protected Void call()
                    throws Exception {
                    
                logger.info("Copying files.");
                Platform.runLater(() -> {
                    copyBtn.setDisable(true);
                    closeBtn.setDisable(true);
                    cancelBtn.setDisable(false);
                    filtersBtn.setDisable(true);
                    zipCheckBox.setDisable(true);
                    selectTargetBtn.setDisable(true);
                });
                
                Set<Path> filteredFiles = applyFileFilters(inputSelectedFiles);    
                Map<Boolean, List<Path>> countsMap = filteredFiles.stream()
                    .collect(Collectors.partitioningBy(p -> Files.isDirectory(p)));
                int dirsCount = countsMap.get(true).size() - 1; // minus root dir
                int filesCount = countsMap.get(false).size();
                logger.info("Filters applied. " +
                    "Directories [" + ((dirsCount < 0) ? 0 : dirsCount) + "], " +
                    "Files [" + filesCount + "].");

                Thread.sleep(100); // pause for n milliseconds
                logger.info("Copy in progress...");

                /*
                 * Walks the source file tree and copies the filtered source
                 * files to the target directory. The directories and files are
                 * copied. In case of any existing directories or files in the
                 * target, they are replaced.
                 */
                Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {

                    /*
                     * Copy the directories.
                     */
                    @Override
                    public FileVisitResult preVisitDirectory(Path dir,
                            BasicFileAttributes attrs)
                            throws IOException {

                        if (isCancelled()) {
                        
                            // Task's isCancelled() method returns true
                            // when its cancel() is executed; in this app
                            // when the Cancel copy button is clicked.
                            // Here, the files copy is terminated.
                            return FileVisitResult.TERMINATE;
                        }
                
                        if (! filteredFiles.contains(dir)) {
                
                            return FileVisitResult.SKIP_SUBTREE;
                        }
                
                        Path target = targetDir.resolve(sourceDir.relativize(dir));

                        try {
                            Files.copy(dir, target);
                            copiedDirsCount++;
                            // Updates the Progess bar using the Task's
                            // updateProgress(workDone, max) method.
                            updateProgress(++currentCounter, dirsCount+filesCount);
                        }
                        catch (FileAlreadyExistsException e) {
                
                            if (! Files.isDirectory(target)) {
                    
                                throw e;
                            }
                        }
                
                        return FileVisitResult.CONTINUE;
                    }
            
                    /*
                     * Copy the files.
                     */
                    @Override
                    public FileVisitResult visitFile(Path file,
                            BasicFileAttributes attrs)
                            throws IOException {
                            
                        if (isCancelled()) {
                        
                            // Task's isCancelled() method
                            // terminates the files copy.
                            return FileVisitResult.TERMINATE;
                        }

                        if (filteredFiles.contains(file)) {
                
                            Files.copy(file,
                                targetDir.resolve(sourceDir.relativize(file)),
                                StandardCopyOption.REPLACE_EXISTING);
                            copiedFilesCount++;
                            // Updates the Progess bar using the Task's
                            // updateProgress(workDone, max) method.
                            updateProgress(++currentCounter, dirsCount+filesCount);
                        }

                        return FileVisitResult.CONTINUE;
                    }
                });
                
                if (zipCheckBox.isSelected()) {

                    if (copiedFilesCount > 0) {
                
                        logger.info("Creating ZIP file, wait... ");
                        Thread.sleep(100);
                        String zipFile = ZipFileCreater.zip(targetDir);
                        logger.info("ZIP file created: " + zipFile);
                    }
                    else {
                        logger.info("Cannot create ZIP file with files count = 0");
                    }
                }

                return null;
            }
        };
        // end copyTask class

        progressBar.progressProperty().bind(copyTask.progressProperty());
        
        new Thread(copyTask).start();    // Run the copy task
        
        // Calling event handlers as task's state is transitioned to
        // SUCCEEDED, FAILED or CANCELLED.
        
        copyTask.setOnFailed(e -> {
            Throwable t = copyTask.getException();
            String message = (t != null) ? t.toString() : "Unknown Exception!";
            logger.info("There was an error during the copy process:");
            logger.info(message);
            doTaskEventCloseRoutine(copyTask);
            //t.printStackTrace();
        });
        
        copyTask.setOnCancelled(e -> {
            logger.info("Copy is cancelled by user.");
            doTaskEventCloseRoutine(copyTask);
        });

        copyTask.setOnSucceeded(e -> {
            logger.info("Copy completed. " +
                        "Directories copied [" +
                        ((copiedDirsCount < 1) ? 0 : copiedDirsCount) + "], " +
                        "Files copied [" + copiedFilesCount + "]");
            doTaskEventCloseRoutine(copyTask);
        });
    }

    private void doTaskEventCloseRoutine(Task copyTask) {
    
        logger.info("Status: " + copyTask.getState() + "\n");
        logger.info("Select a target directory, apply file filters and copy.");
        Platform.runLater(() -> {
            selectTargetBtn.setDisable(false);
            closeBtn.setDisable(false);
            cancelBtn.setDisable(true);
        });    
    }
    
    /*
     * Sets the file filters to its default value in case the filters dialog
     * is not opened at all. Otherwise the already set value is used. Apply
     * the file filters; the filtered files are returned as a Set collection.
     */    
    private Set<Path> applyFileFilters(Set<Path> selectedFiles)
            throws IOException {
    
        if (fileFilters == null) {

            fileFilters = FileFilters.getDefault();
            logger.info("File filters: " + fileFilters.toString());
        }
        
        return new FileFilterApplication().apply(sourceDir, 
                                                    selectedFiles,
                                                    fileFilters);
    }
}

DateOption.java

package com.javaquizplayer.examples.copyfilesapp;

import java.util.Map;
import java.util.HashMap;
import java.util.EnumSet;
import java.util.stream.Collectors;
import java.util.function.Function;


/*
 * Enum class represents the date file filter options.
 * Also see FileFilters and FileFiltersDialog.
 */
public enum DateOption {

    ALL_DAYS,
    TODAY,
    LAST_7_DAYS,
    LAST_30_DAYS;

    @Override
    public String toString() {

        return getFormattedString(super.toString());
    }

    /*
     * Formats enum's string, for example: from LAST_7_DAYS to: Last 7 days.
     * Replaces the underscore ("_") with a blank space. Changes the case to
     * first letter upper and remaining lower case.
     */
    private static String getFormattedString(String input) {

        String s = input.toLowerCase();
        s = s.replace("_", " ");
        return (s.substring(0, 1).toUpperCase() + s.substring(1, s.length()));
    }
    
    /* Map with formatted string as key and enum constant as value */
    private static Map<String, DateOption> map;
    
    /* Initially, populates the map */
    static {
        map = EnumSet.allOf(DateOption.class)
                .stream()
                    .collect(Collectors.toMap(
                        d -> getFormattedString(d.toString()), Function.identity()));
    }
    
    /*
     * The lookup for the map. Returns the enum constant for the
     * given string representing the constant.
     */
    public static DateOption lookup(String s) { 
    
        return map.get(s); 
    }
}

FileFilters.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;


/*
 * This class represents the file filters data. This data is
 * captured in the FileFiltersDialog and is applied in the files
 * copy routine.
 */
public class FileFilters {


    private boolean allFiles;
    private DateOption dateOpt;
    private ObservableList<String> fileTypes;
    
    /* 
     * List of file extensions for selection. "All" specifies that select
     * all file types. The "" (empty string) specifies that the file has no
     * extension.
     */
    public static final String [] FILE_EXTENSIONS =
            {"All", "java", "class", "txt", "doc", "docx", "xls",
             "xlsx", "ppt", "png", "jpg", "pdf", "jar", "exe", "html",
             "xhtml", "htm", "mp3", "wmv", ""};


    /*
     * Constructor with default values.
     */
    public FileFilters() {
    
        allFiles = false;
        dateOpt = DateOption.ALL_DAYS;
        fileTypes = FXCollections.observableArrayList("All");
    }
    
    /*
     * Returns an instance of FileFilters with following options:
     * All files for all days.
     */
    public static FileFilters getDefault() {
    
        return new FileFilters();
    }

    public void setAllFiles(boolean b) {
    
        allFiles = b;
    }
    public boolean getAllFiles() {
    
        return allFiles;
    }
    
    public void setDateOption(DateOption d) {
    
        dateOpt = d;
    }
    public DateOption getDateOption() {
    
        return dateOpt;
    }
    
    public void setFileTypes(ObservableList<String> types) {
    
        fileTypes = types;
    }
    public ObservableList<String> getFileTypes() {
    
        return fileTypes;
    }
    
    @Override
    public String toString() {

        return dateOpt.toString() + ", " + fileTypes.toString();
    }
}

FileFilterDialog.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.scene.layout.VBox;
import javafx.scene.layout.HBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.geometry.Orientation;
import javafx.geometry.Insets;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;


/*
 * This class has a single method which builds a dialog to capture file
 * filter options. The method returns a dialog with an instance of
 * FileFilters with the selected input or a default instance in case the
 * dialog is cancelled.
 */
public class FileFilterDialog {


    public Dialog<FileFilters> create() {

        Dialog<FileFilters> dialog = new Dialog<>();
        dialog.setTitle("File Filters");
        dialog.setResizable(false);
    
        CheckBox allCheckBox = new CheckBox("Select all files");
        
        ToggleGroup radioGroup = new ToggleGroup();
        RadioButton radio1 = new RadioButton(DateOption.ALL_DAYS.toString());
        radio1.setSelected(true);
        RadioButton radio2 = new RadioButton(DateOption.TODAY.toString());
        RadioButton radio3 = new RadioButton(DateOption.LAST_7_DAYS.toString());
        RadioButton radio4 = new RadioButton(DateOption.LAST_30_DAYS.toString());
        radio1.setToggleGroup(radioGroup);
        radio2.setToggleGroup(radioGroup);
        radio3.setToggleGroup(radioGroup);
        radio4.setToggleGroup(radioGroup);    
    
        HBox radioHb = new HBox(15);
        radioHb.getChildren().addAll(radio1, radio2, radio3, radio4);
        
        ListView<String> fileTypesList = new ListView<>();
        fileTypesList.setPrefHeight(50.0d);
        fileTypesList.setItems(FXCollections.observableArrayList(FileFilters.FILE_EXTENSIONS));
        fileTypesList.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        fileTypesList.setOrientation(Orientation.HORIZONTAL);
        
        allCheckBox.selectedProperty().addListener(
            (ObservableValue<? extends Boolean> ov, Boolean oldVal,
                                                    Boolean newVal) -> {

                fileTypesList.getSelectionModel().clearSelection();
                fileTypesList.getSelectionModel().selectFirst();
                fileTypesList.setDisable(newVal);
                radio1.setSelected(true);
                radio1.setDisable(newVal);
                radio2.setDisable(newVal);
                radio3.setDisable(newVal);
                radio4.setDisable(newVal);
        });
        
        VBox vb = new VBox(20);
        vb.setPadding(new Insets(20));
        vb.getChildren().addAll(allCheckBox, radioHb, fileTypesList);
        
        ButtonType okButtonType = new ButtonType("Okay", ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().add(okButtonType);
        Button okBtn = (Button) dialog.getDialogPane().lookupButton(okButtonType);
        
        dialog.setResultConverter((ButtonType b) -> {

            if (b == okButtonType) {

                FileFilters ff = new FileFilters();
                ff.setAllFiles(allCheckBox.isSelected());
                String s = ((RadioButton) radioGroup.getSelectedToggle()).getText();    
                ff.setDateOption(DateOption.lookup(s));
                ff.setFileTypes(fileTypesList.getSelectionModel().getSelectedItems());
                return ff;
            }

            return FileFilters.getDefault();
        });

        dialog.getDialogPane().setContent(vb);
        fileTypesList.getSelectionModel().selectFirst();

        return dialog;
    }
}

FileFilterApplication.java

package com.javaquizplayer.examples.copyfilesapp;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.util.Set;
import java.util.HashSet;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.ZoneId;
import java.time.Instant;
import java.time.LocalDate;


/*
 * Class has a single method which returns the collection of
 * Path objects after the file filters are applied. Additionally,
 * any empty directories are removed from the filtered collection.
 * The input is the selected files, source direcory and the
 * file filters.
 */
public class FileFilterApplication {


    public FileFilterApplication() {
    }

    public Set<Path> apply(Path sourceDir,
                            Set<Path> selectedFiles,
                            FileFilters filters)
            throws IOException {

        Set<Path> filteredFiles = new HashSet<>();

        Files.walkFileTree(sourceDir, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult preVisitDirectory(Path dir,
                                                        BasicFileAttributes attrs)
                    throws IOException {
                
                if (! selectedFiles.contains(dir)) {

                    // Not a selected directory, skip it
                    return FileVisitResult.SKIP_SUBTREE;
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file,
                                                BasicFileAttributes attrs)
                    throws IOException {

                if (selectedFiles.contains(file) &&
                        applyFileTypeFilter(filters, file) &&
                            applyDateOptionFilter(filters, file))  {

                    // Add selected files that match the
                    // file filter criteria
                    filteredFiles.add(file);
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir,
                                                        IOException exc)
                    throws IOException {

                if (dirHasFiles(dir, filteredFiles)) {

                    // Add directories with files in it
                    filteredFiles.add(dir);
                }

                return FileVisitResult.CONTINUE;
            }
        });

        return filteredFiles;
    }
    
    /*
     * Check if the input dir has any files in it that are already
     * filtered. Note this check is not on the file system.
     */
    private boolean dirHasFiles(Path inDir, Set<Path> list) {

        return list
                .stream()
                  .anyMatch(p -> ((!p.equals(inDir)) && p.startsWith(inDir)));
    }

    private boolean applyFileTypeFilter(FileFilters filters, Path file) {
    
        return filters.getFileTypes().contains("All") ||
            filters.getFileTypes().contains(getFileExtension(file));
    }

    private String getFileExtension(Path file) {

        String fileName = file.getFileName().toString();
        int ix = fileName.lastIndexOf(".");
        return (ix == -1) ? "" : fileName.substring(ix + 1);
    }
    
    private boolean applyDateOptionFilter(FileFilters filters, Path file)
            throws IOException  {

        boolean returnValue = false;
    
        switch (filters.getDateOption()) {

            case TODAY:
                returnValue = getFileDate(file).isEqual(LocalDate.now());
                break;
            case LAST_7_DAYS:
                returnValue = getFileDate(file).isAfter(LocalDate.now().minusDays(7));
                break;
            case LAST_30_DAYS:
                returnValue = getFileDate(file).isAfter(LocalDate.now().minusDays(30));
                break;
            default:
                returnValue = true;
        }    
    
        return returnValue;
    }
    
    /*
     * Returns file's last modified date as a LocalDate.
     */
    private LocalDate getFileDate(Path file)
            throws IOException {
    
        Instant fileTime = Files.getLastModifiedTime(file).toInstant();    
        return fileTime.atZone(ZoneId.systemDefault()).toLocalDate();
    }
}

AppStarter.java

package com.javaquizplayer.examples.copyfilesapp;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.stream.Stream;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.Handler;


/*
 * The copy files app's starter program.
 * Launches the app and the main GUI.
 */
public class AppStarter extends Application {


    private static Logger logger;
    private static final String LOG_FILE = "copy_files_app_log_%g.txt";


    public static void main(String... args) {

        Application.launch(args);
    }
    
    /*
     * Does some initial configuration for the app.
     * Configures logger and its handlers.
     * Exits the application if there is an exception.
     */
    @Override
    public void init() {
    
        try {
            configureLogging();
        }
        catch (IOException ex) {
        
            System.out.println("An IOException occurred during app initialization.");
            System.out.println("This happened during the logger configuration.");
            System.out.println("See the stack trace below:");
            ex.printStackTrace();
            Platform.exit(); // NOTE: doesn't run the stop() method.
        }
    }

    /*
     * Configures the logger and registers a file handler.
     */
    private void configureLogging()
            throws IOException {
    
        System.setProperty("java.util.logging.SimpleFormatter.format",
                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS  %5$s%n");
        logger = Logger.getLogger("copy_app_logger");
        logger.setUseParentHandlers(false); // disables console logging
        logger.addHandler(new LogFileHandler(LOG_FILE));
        logger.setLevel(Level.INFO);
        logger.info("Logger is configured");
    }
    
    /*
     * Displays the main GUI.
     */
    @Override
    public void start(Stage primaryStage) {

        logger.info("Launching the GUI");
        new FileTreeView(primaryStage);
    }
    
    /*
     * Does cleanup and releases resources.
     */
    @Override
    public void stop() {
        
        logger.info("Closing the app");
        
        // Close the logger's file and stream handlers
        Stream.of(logger.getHandlers()).forEach(h -> h.close());
    }
}

LogFileHandler.java

package com.javaquizplayer.examples.copyfilesapp;

import java.util.logging.SimpleFormatter;
import java.io.IOException;

/*
 * Handler to write log messages to a log file.
 */
public class LogFileHandler extends FileHandler {


    public LogFileHandler(String pattern)
            throws IOException {
        
        super(pattern);
        setFormatter(new SimpleFormatter()); // overrides the default xml formatter
    }
}

TextAreaLogHandler.java

package com.javaquizplayer.examples.copyfilesapp;

import java.util.logging.StreamHandler;
import java.util.logging.LogRecord;
import javafx.scene.control.TextArea;
import javafx.application.Platform;


/*
 * Handler to write log messages to the app's GUI (status message area,
 * a TextArea control, of the CopyDialog.java).
 */
public class TextAreaLogHandler extends StreamHandler {


    TextArea textArea = null;

    public TextAreaLogHandler(TextArea textArea) {
        
        this.textArea = textArea;
    }

    @Override
    public void publish(LogRecord record) {
        
        super.publish(record);
        flush();
        Platform.runLater(() -> textArea.appendText(getFormatter().format(record)));
    }
}

ZipFileCreater.java

package com.javaquizplayer.examples.copyfilesapp;

import java.util.zip.ZipOutputStream;
import java.util.zip.ZipEntry;
import java.io.IOException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;
import java.nio.file.FileVisitResult;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;


/*
 * Class has one static method.
 * Creates a ZIP (compressed archive) file for the supplied directory with
 * files. This directory is the same as the target directory as a result of
 * copy action in Copy Dialog.
 * The ZIP file created is in the same directory in which the supplied directory
 * is present.
 */
public class ZipFileCreater {

    
    public static String zip(Path input)
            throws IOException {

        String targetFileNameStr = input.getFileName().toString() + ".zip";
        Path targetPath =
                Paths.get(input.getParent().toString(), targetFileNameStr);
        ZipOutputStream zipOutputStream =
                new ZipOutputStream(new FileOutputStream(targetPath.toString()));

        Files.walkFileTree(input, new SimpleFileVisitor<Path>() {

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                
                Path targetfile = input.relativize(file);
                ZipEntry zipEntry = new ZipEntry(targetfile.toString());
                zipOutputStream.putNextEntry(zipEntry);
                
                try(FileInputStream fileInputStream =
                        new FileInputStream(file.toString())) {
                
                    byte [] buf = new byte [512];
                    int bytesRead;
                
                    while ((bytesRead = fileInputStream.read(buf)) > 0) {
                
                        zipOutputStream.write(buf, 0, bytesRead);
                    }
                }
                
                zipOutputStream.closeEntry();
                return FileVisitResult.CONTINUE;
            }
        });
        
        zipOutputStream.close();
        return targetPath.toString();
    }
}

Return to top


This page uses Java code formatting from http://hilite.me/.