AbstractImageRenamer.java

/*
 * Copyright 2011, Aiki IT, FotoRenamer
 * <p/>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.aikiit.fotorenamer.image;

import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;
import de.aikiit.fotorenamer.exception.InvalidDirectoryException;
import de.aikiit.fotorenamer.exception.NoFilesFoundException;
import de.aikiit.fotorenamer.gui.ProgressBar;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

import static de.aikiit.fotorenamer.util.LocalizationHelper.getBundleString;
import static de.aikiit.fotorenamer.util.LocalizationHelper.getParameterizedBundleString;

/**
 * Abstract class that handles image renaming and file handling.
 * <br>
 * The onliest abstract method generates a filename from a given file
 * and should be used to provide different strategies for image renaming.
 *
 * @author hirsch
 * @version 2011-03-22, 11:43
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
abstract class AbstractImageRenamer implements Runnable {

    /**
     * The logger of this class.
     */
    private static final Logger LOG = LogManager.getLogger(AbstractImageRenamer.class);

    /**
     * The currently selected directory to work on.
     */
    private File currentDirectory = null;
    /**
     * The list of all relevant files in the current directory.
     */
    private List<File> imageList = null;

    /**
     * Progress bar for visual feedback of what's going on.
     */
    private ProgressBar progressBar = null;
    /**
     * Number of files that need processing.
     */
    private int amountOfFiles = 0;

    /**
     * Starts image processing on the given directory if it contains
     * relevant images. The strategy of renaming is defined by
     * subclasses implementation of @see #renameImage(File).
     *
     * @param directory Name of directory to work on.
     * @throws InvalidDirectoryException If there's a problem with
     *                                   the selected directory.
     * @throws NoFilesFoundException     If the selected directory is empty.
     */
    AbstractImageRenamer(final String directory) throws InvalidDirectoryException, NoFilesFoundException {

        if (directory == null) {
            throw new InvalidDirectoryException("null is not a directory");
        }

        this.currentDirectory = new File(directory);
        if (!this.currentDirectory.isDirectory()) {
            throw new InvalidDirectoryException(this.currentDirectory);
        }

        // retrieve relevant images in directory
        File[] files = this.currentDirectory.listFiles(new ImageFilenameFilter());
        if (files == null || files.length == 0) {
            throw new NoFilesFoundException(this.currentDirectory);
        }
        this.imageList = Lists.newArrayList(files);
        this.amountOfFiles = this.imageList.size();
    }

    /**
     * Performs the actual/technical renaming.
     */
    private void renameFiles() {
        LOG.info("Starting to rename {} files.", this.amountOfFiles);

        Consumer<File> consumer = file -> {
            // extract EXIF data and fetch target filename
            String targetFilename = renameImage(file);

            // update progress bar (names have a different length)
            progressBar.setProgress();
            progressBar.setText(file.getName());
            progressBar.updateUI();

            // TODO add second progressbar or counter for errors
            try {
                Files.move(file.toPath(), new File(file.getParent(), targetFilename).toPath());
            } catch (IOException e) {
                LOG.error("Unable to rename '{}' to '{}'", file.getName(), targetFilename);
            }
        };
        Predicate<File> fileOnly = file -> file != null && file.isFile();

        this.imageList.parallelStream().filter(fileOnly).forEach(consumer);
    }

    /**
     * This method provides a strategy to rename image files when
     * iterating over a list of files.
     * It is called during image processing.
     *
     * @param imageFile Filename to renameFiles according to the subclass
     *                  implementation.
     * @return New filename for the given file.
     */
    protected abstract String renameImage(File imageFile);

    /**
     * Performs the renaming and updates the UI. All error handling is done in
     * other methods.
     *
     * @see #renameFiles()
     */
    public final void run() {
        this.progressBar = new ProgressBar(this.amountOfFiles);

        try {
            renameFiles();
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, getParameterizedBundleString("fotorenamer.ui.rename.error", MoreObjects.firstNonNull(e.getMessage(), e.getClass().getSimpleName())), getBundleString("fotorenamer.ui.rename.error.title"), JOptionPane.ERROR_MESSAGE);

            this.amountOfFiles = 0;
        } finally {
            this.progressBar.dispose();
        }

        // show UI-notification
        StringBuilder notification = new StringBuilder();
        switch (this.amountOfFiles) {
            case 0:
                notification.append(getParameterizedBundleString("fotorenamer.ui.rename.success.message.none", this.currentDirectory.getName()));
                break;
            case 1:
                notification.append(getParameterizedBundleString("fotorenamer.ui.rename.success.message.one", this.currentDirectory.getName()));
                break;
            default:
                notification.append(getParameterizedBundleString("fotorenamer.ui.rename.success.message", this.amountOfFiles, this.currentDirectory.getName()));
                break;
        }

        notification.append("\n\n");
        JOptionPane.showMessageDialog(null, notification.toString(), getBundleString("fotorenamer.ui.rename.success.title"), JOptionPane.INFORMATION_MESSAGE);
    }
}