/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.itemexport;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.mail.MessagingException;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.itemexport.service.ItemExportService;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchemaEnum;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.CommunityService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.Email;
import org.dspace.core.I18nUtil;
import org.dspace.core.LogHelper;
import org.dspace.core.Utils;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.handle.service.HandleService;
import org.dspace.scripts.handler.DSpaceRunnableHandler;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Item exporter to create simple AIPs for DSpace content. Currently exports
 * individual items, or entire collections. For instructions on use, see
 * printUsage() method.
 * <p>
 * ItemExport creates the simple AIP package that the importer also uses. It
 * consists of:
 * <pre>{@code
 * /exportdir/42/ (one directory per item)
 *              / dublin_core.xml - qualified dublin core in RDF schema
 *              / contents - text file, listing one file per line
 *              / file1 - files contained in the item
 *              / file2
 *              / ...
 * }</pre>
 * <p>
 * issues -doesn't handle special characters in metadata (needs to turn {@code &'s} into
 * {@code &amp;}, etc.)
 * <p>
 * Modified by David Little, UCSD Libraries 12/21/04 to allow the registration
 * of files (bitstreams) into DSpace.
 *
 * @author David Little
 * @author Jay Paz
 */
public class ItemExportServiceImpl implements ItemExportService {
    protected final int SUBDIR_LIMIT = 0;

    @Autowired(required = true)
    protected BitstreamService bitstreamService;
    @Autowired(required = true)
    protected CommunityService communityService;
    @Autowired(required = true)
    protected EPersonService ePersonService;
    @Autowired(required = true)
    protected ItemService itemService;
    @Autowired(required = true)
    protected HandleService handleService;
    @Autowired(required = true)
    protected ConfigurationService configurationService;

    /**
     * log4j logger
     */
    private final Logger log = org.apache.logging.log4j.LogManager.getLogger();

    private DSpaceRunnableHandler handler;

    protected ItemExportServiceImpl() {

    }


    @Override
    public void exportItem(Context c, Iterator<Item> i,
                           String destDirName, int seqStart, boolean migrate,
                           boolean excludeBitstreams) throws Exception {
        int mySequenceNumber = seqStart;
        int counter = SUBDIR_LIMIT - 1;
        int subDirSuffix = 0;
        String fullPath = destDirName;
        String subdir = "";
        File dir;

        if (SUBDIR_LIMIT > 0) {
            dir = new File(destDirName);
            if (!dir.isDirectory()) {
                throw new IOException(destDirName + " is not a directory.");
            }
        }

        logInfo("Beginning export");

        while (i.hasNext()) {
            if (SUBDIR_LIMIT > 0 && ++counter == SUBDIR_LIMIT) {
                subdir = Integer.toString(subDirSuffix++);
                fullPath = destDirName + File.separatorChar + subdir;
                counter = 0;

                if (!new File(fullPath).mkdirs()) {
                    throw new IOException("Error, can't make dir " + fullPath);
                }
            }

            logInfo("Exporting item to " + mySequenceNumber);
            Item item = i.next();
            exportItem(c, item, fullPath, mySequenceNumber, migrate, excludeBitstreams);
            c.uncacheEntity(item);
            mySequenceNumber++;
        }
    }

    protected void exportItem(Context c, Item myItem, String destDirName,
                              int seqStart, boolean migrate, boolean excludeBitstreams) throws Exception {
        File destDir = new File(destDirName);

        if (destDir.exists()) {
            // now create a subdirectory
            File itemDir = new File(destDir + "/" + seqStart);

            logInfo("Exporting Item " + myItem.getID() +
                                   (myItem.getHandle() != null ? ", handle " + myItem.getHandle() : "") +
                                   " to " + itemDir);

            if (itemDir.exists()) {
                throw new Exception("Directory " + destDir + "/" + seqStart
                                        + " already exists!");
            }

            if (itemDir.mkdir()) {
                // make it this far, now start exporting
                writeMetadata(c, myItem, itemDir, migrate);
                writeBitstreams(c, myItem, itemDir, excludeBitstreams);
                writeCollections(myItem, itemDir);
                if (!migrate) {
                    writeHandle(c, myItem, itemDir);
                }
            } else {
                throw new Exception("Error, can't make dir " + itemDir);
            }
        } else {
            throw new Exception("Error, directory " + destDirName
                                    + " doesn't exist!");
        }
    }

    /**
     * Discover the different schemas in use and output a separate metadata XML
     * file for each schema.
     *
     * @param c       DSpace context
     * @param i       DSpace Item
     * @param destDir destination directory
     * @param migrate Whether to use the migrate option or not
     * @throws Exception if error
     */
    protected void writeMetadata(Context c, Item i, File destDir, boolean migrate)
        throws Exception {
        Set<String> schemas = new HashSet<>();
        List<MetadataValue> dcValues = itemService.getMetadata(i, Item.ANY, Item.ANY, Item.ANY, Item.ANY);
        for (MetadataValue metadataValue : dcValues) {
            schemas.add(metadataValue.getMetadataField().getMetadataSchema().getName());
        }

        // Save each of the schemas into it's own metadata file
        for (String schema : schemas) {
            writeMetadata(c, schema, i, destDir, migrate);
        }
    }

    /**
     * output the item's dublin core into the item directory
     *
     * @param c       DSpace context
     * @param schema  schema
     * @param i       DSpace Item
     * @param destDir destination directory
     * @param migrate Whether to use the migrate option or not
     * @throws Exception if error
     */
    protected void writeMetadata(Context c, String schema, Item i,
                                 File destDir, boolean migrate) throws Exception {
        String filename;
        if (schema.equals(MetadataSchemaEnum.DC.getName())) {
            filename = "dublin_core.xml";
        } else {
            filename = "metadata_" + schema + ".xml";
        }

        File outFile = new File(destDir, filename);

        logInfo("Attempting to create file " + outFile);

        if (outFile.createNewFile()) {
            BufferedOutputStream out = new BufferedOutputStream(
                new FileOutputStream(outFile));

            List<MetadataValue> dcorevalues = itemService.getMetadata(i, schema, Item.ANY, Item.ANY,
                                                                      Item.ANY);

            // XML preamble
            byte[] utf8 = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n"
                .getBytes("UTF-8");
            out.write(utf8, 0, utf8.length);

            String dcTag = "<dublin_core schema=\"" + schema + "\">\n";
            utf8 = dcTag.getBytes("UTF-8");
            out.write(utf8, 0, utf8.length);

            String dateIssued = null;
            String dateAccessioned = null;

            for (MetadataValue dcv : dcorevalues) {
                MetadataField metadataField = dcv.getMetadataField();
                String qualifier = metadataField.getQualifier();

                if (qualifier == null) {
                    qualifier = "none";
                }

                String language = dcv.getLanguage();

                if (language != null) {
                    language = " language=\"" + language + "\"";
                } else {
                    language = "";
                }

                utf8 = ("  <dcvalue element=\"" + metadataField.getElement() + "\" "
                    + "qualifier=\"" + qualifier + "\""
                    + language + ">"
                    + Utils.addEntities(dcv.getValue()) + "</dcvalue>\n")
                    .getBytes("UTF-8");

                if (!migrate ||
                    (migrate && !(
                        ("date".equals(metadataField.getElement()) && "issued".equals(qualifier)) ||
                            ("date".equals(metadataField.getElement()) && "accessioned".equals(qualifier)) ||
                            ("date".equals(metadataField.getElement()) && "available".equals(qualifier)) ||
                            ("identifier".equals(metadataField.getElement()) && "uri".equals(qualifier) &&
                                (dcv.getValue() != null && dcv.getValue().startsWith(
                                    handleService.getCanonicalPrefix() + handleService.getPrefix() + "/"))) ||
                            ("description".equals(metadataField.getElement()) && "provenance".equals(qualifier)) ||
                            ("format".equals(metadataField.getElement()) && "extent".equals(qualifier)) ||
                            ("format".equals(metadataField.getElement()) && "mimetype".equals(qualifier))))) {
                    out.write(utf8, 0, utf8.length);
                }

                // Store the date issued and accession to see if they are different
                // because we need to keep date.issued if they are, when migrating
                if (("date".equals(metadataField.getElement()) && "issued".equals(qualifier))) {
                    dateIssued = dcv.getValue();
                }
                if (("date".equals(metadataField.getElement()) && "accessioned".equals(qualifier))) {
                    dateAccessioned = dcv.getValue();
                }
            }

            // When migrating, only keep date.issued if it is different to date.accessioned
            if (migrate &&
                (dateIssued != null) &&
                (dateAccessioned != null) &&
                !dateIssued.equals(dateAccessioned)) {
                utf8 = ("  <dcvalue element=\"date\" "
                    + "qualifier=\"issued\">"
                    + Utils.addEntities(dateIssued) + "</dcvalue>\n")
                    .getBytes("UTF-8");
                out.write(utf8, 0, utf8.length);
            }

            utf8 = "</dublin_core>\n".getBytes("UTF-8");
            out.write(utf8, 0, utf8.length);

            out.close();
        } else {
            throw new Exception("Cannot create dublin_core.xml in " + destDir);
        }
    }

    /**
     * create the file 'handle' which contains the handle assigned to the item
     *
     * @param c       DSpace Context
     * @param i       DSpace Item
     * @param destDir destination directory
     * @throws Exception if error
     */
    protected void writeHandle(Context c, Item i, File destDir)
        throws Exception {
        if (i.getHandle() == null) {
            return;
        }
        String filename = "handle";

        File outFile = new File(destDir, filename);

        if (outFile.createNewFile()) {
            PrintWriter out = new PrintWriter(new FileWriter(outFile, StandardCharsets.UTF_8));

            out.println(i.getHandle());

            // close the contents file
            out.close();
        } else {
            throw new Exception("Cannot create file " + filename + " in "
                                    + destDir);
        }
    }

    /**
     * Create the 'collections' file.  List handles of all Collections which
     * contain this Item.  The "owning" Collection is listed first.
     *
     * @param item list collections holding this Item.
     * @param destDir write the file here.
     * @throws IOException if the file cannot be created or written.
     */
    protected void writeCollections(Item item, File destDir)
            throws IOException {
        File outFile = new File(destDir, "collections");
        if (outFile.createNewFile()) {
            try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) {
                String ownerHandle = item.getOwningCollection().getHandle();
                out.println(ownerHandle);
                for (Collection collection : item.getCollections()) {
                    String collectionHandle = collection.getHandle();
                    if (!collectionHandle.equals(ownerHandle)) {
                        out.println(collectionHandle);
                    }
                }
            }
        } else {
            throw new IOException("Cannot create 'collections' in " + destDir);
        }
    }

    /**
     * Create both the bitstreams and the contents file. Any bitstreams that
     * were originally registered will be marked in the contents file as such.
     * However, the export directory will contain actual copies of the content
     * files being exported.
     *
     * @param c                 the DSpace context
     * @param i                 the item being exported
     * @param destDir           the item's export directory
     * @param excludeBitstreams whether to exclude bitstreams
     * @throws Exception if error
     *                   if there is any problem writing to the export directory
     */
    protected void writeBitstreams(Context c, Item i, File destDir,
                                   boolean excludeBitstreams) throws Exception {
        File outFile = new File(destDir, "contents");

        if (outFile.createNewFile()) {
            PrintWriter out = new PrintWriter(new FileWriter(outFile, StandardCharsets.UTF_8));

            List<Bundle> bundles = i.getBundles();

            for (Bundle bundle : bundles) {
                // bundles can have multiple bitstreams now...
                List<Bitstream> bitstreams = bundle.getBitstreams();

                String bundleName = bundle.getName();

                for (Bitstream bitstream : bitstreams) {
                    String myName = bitstream.getName();
                    String oldName = myName;

                    String description = bitstream.getDescription();
                    if (!StringUtils.isEmpty(description)) {
                        description = "\tdescription:" + description;
                    } else {
                        description = "";
                    }

                    String primary = "";
                    if (bitstream.equals(bundle.getPrimaryBitstream())) {
                        primary = "\tprimary:true ";
                    }

                    int myPrefix = 1; // only used with name conflict

                    boolean isDone = false; // done when bitstream is finally
                    // written

                    while (!excludeBitstreams && !isDone) {
                        if (myName.contains(File.separator)) {
                            String dirs = myName.substring(0, myName
                                .lastIndexOf(File.separator));
                            File fdirs = new File(destDir + File.separator
                                                      + dirs);
                            if (!fdirs.exists() && !fdirs.mkdirs()) {
                                logError("Unable to create destination directory");
                            }
                        }

                        File fout = new File(destDir, myName);

                        if (fout.createNewFile()) {
                            InputStream is = bitstreamService.retrieve(c, bitstream);
                            FileOutputStream fos = new FileOutputStream(fout);
                            Utils.bufferedCopy(is, fos);
                            // close streams
                            is.close();
                            fos.close();

                            isDone = true;
                        } else {
                            myName = myPrefix + "_" + oldName; // keep
                            // appending
                            // numbers to the
                            // filename until
                            // unique
                            myPrefix++;
                        }
                    }

                    // write the manifest file entry
                    if (bitstreamService.isRegisteredBitstream(bitstream)) {
                        out.println("-r -s " + bitstream.getStoreNumber()
                                        + " -f " + myName +
                                        "\tbundle:" + bundleName +
                                        primary + description);
                    } else {
                        out.println(myName + "\tbundle:" + bundleName +
                                        primary + description);
                    }

                }
            }

            // close the contents file
            out.close();
        } else {
            throw new Exception("Cannot create contents in " + destDir);
        }
    }

    @Override
    public void exportAsZip(Context context, Iterator<Item> items,
                            String destDirName, String zipFileName,
                            int seqStart, boolean migrate,
                            boolean excludeBitstreams) throws Exception {
        String workDir = getExportWorkDirectory() +
            System.getProperty("file.separator") +
            zipFileName;

        File wkDir = new File(workDir);
        if (!wkDir.exists() && !wkDir.mkdirs()) {
            logError("Unable to create working directory");
        }

        File dnDir = new File(destDirName);
        if (!dnDir.exists() && !dnDir.mkdirs()) {
            logError("Unable to create destination directory");
        }

        try {
            // export the items using normal export method (this exports items to our workDir)
            exportItem(context, items, workDir, seqStart, migrate, excludeBitstreams);

            // now zip up the workDir directory created above
            zip(workDir, destDirName + System.getProperty("file.separator") + zipFileName);
        } finally {
            // Cleanup workDir created above, if it still exists
            if (wkDir.exists()) {
                deleteDirectory(wkDir);
            }
        }
    }

    @Override
    public void createDownloadableExport(DSpaceObject dso,
                                         Context context, boolean migrate) throws Exception {
        EPerson eperson = context.getCurrentUser();
        ArrayList<DSpaceObject> list = new ArrayList<>(1);
        list.add(dso);
        processDownloadableExport(list, context, eperson == null ? null
            : eperson.getEmail(), migrate);
    }

    @Override
    public void createDownloadableExport(List<DSpaceObject> dsObjects,
                                         Context context, boolean migrate) throws Exception {
        EPerson eperson = context.getCurrentUser();
        processDownloadableExport(dsObjects, context, eperson == null ? null
            : eperson.getEmail(), migrate);
    }

    @Override
    public void createDownloadableExport(DSpaceObject dso,
                                         Context context, String additionalEmail, boolean migrate) throws Exception {
        ArrayList<DSpaceObject> list = new ArrayList<>(1);
        list.add(dso);
        processDownloadableExport(list, context, additionalEmail, migrate);
    }

    @Override
    public void createDownloadableExport(List<DSpaceObject> dsObjects,
                                         Context context, String additionalEmail, boolean migrate) throws Exception {
        processDownloadableExport(dsObjects, context, additionalEmail, migrate);
    }

    /**
     * Does the work creating a List with all the Items in the Community or
     * Collection It then kicks off a new Thread to export the items, zip the
     * export directory and send confirmation email
     *
     * @param dsObjects       - List of dspace objects to process
     * @param context         - the dspace context
     * @param additionalEmail - email address to cc in addition the the current user email
     * @param toMigrate       Whether to use the migrate option or not
     * @throws Exception if error
     */
    protected void processDownloadableExport(List<DSpaceObject> dsObjects,
                                             Context context, final String additionalEmail, boolean toMigrate)
        throws Exception {
        final EPerson eperson = context.getCurrentUser();
        final boolean migrate = toMigrate;

        // before we create a new export archive lets delete the 'expired'
        // archives
        //deleteOldExportArchives(eperson.getID());
        deleteOldExportArchives();

        // keep track of the commulative size of all bitstreams in each of the
        // items
        // it will be checked against the config file entry
        double size = 0;
        final HashMap<String, List<UUID>> itemsMap = new HashMap<>();
        for (DSpaceObject dso : dsObjects) {
            if (dso.getType() == Constants.COMMUNITY) {
                Community community = (Community) dso;
                // get all the collections in the community
                List<Collection> collections = communityService.getAllCollections(context, community);
                for (Collection collection : collections) {
                    ArrayList<UUID> items = new ArrayList<>();
                    // get all the items in each collection
                    Iterator<Item> iitems = itemService.findByCollection(context, collection);
                    try {
                        while (iitems.hasNext()) {
                            Item item = iitems.next();
                            // get all the bundles in the item
                            List<Bundle> bundles = item.getBundles();
                            for (Bundle bundle : bundles) {
                                // get all the bitstreams in each bundle
                                List<Bitstream> bitstreams = bundle.getBitstreams();
                                for (Bitstream bitstream : bitstreams) {
                                    // add up the size
                                    size += bitstream.getSizeBytes();
                                }
                            }
                            items.add(item.getID());
                        }
                    } finally {
                        if (items.size() > 0) {
                            itemsMap.put("collection_" + collection.getID(), items);
                        }
                    }
                }
            } else if (dso.getType() == Constants.COLLECTION) {
                Collection collection = (Collection) dso;
                ArrayList<UUID> items = new ArrayList<>();

                // get all the items in the collection
                Iterator<Item> iitems = itemService.findByCollection(context, collection);
                try {
                    while (iitems.hasNext()) {
                        Item item = iitems.next();
                        // get all thebundles in the item
                        List<Bundle> bundles = item.getBundles();
                        for (Bundle bundle : bundles) {
                            // get all the bitstreams in the bundle
                            List<Bitstream> bitstreams = bundle.getBitstreams();
                            for (Bitstream bitstream : bitstreams) {
                                // add up the size
                                size += bitstream.getSizeBytes();
                            }
                        }
                        items.add(item.getID());
                    }
                } finally {
                    if (items.size() > 0) {
                        itemsMap.put("collection_" + collection.getID(), items);
                    }
                }
            } else if (dso.getType() == Constants.ITEM) {
                Item item = (Item) dso;
                // get all the bundles in the item
                List<Bundle> bundles = item.getBundles();
                for (Bundle bundle : bundles) {
                    // get all the bitstreams in the bundle
                    List<Bitstream> bitstreams = bundle.getBitstreams();
                    for (Bitstream bitstream : bitstreams) {
                        // add up the size
                        size += bitstream.getSizeBytes();
                    }
                }
                ArrayList<UUID> items = new ArrayList<>();
                items.add(item.getID());
                itemsMap.put("item_" + item.getID(), items);
            } else {
                // nothing to do just ignore this type of DSpaceObject
            }
        }

        // check the size of all the bitstreams against the configuration file
        // entry if it exists
        String megaBytes = configurationService
            .getProperty("org.dspace.app.itemexport.max.size");
        if (megaBytes != null) {
            float maxSize = 0;
            try {
                maxSize = Float.parseFloat(megaBytes);
            } catch (Exception e) {
                // ignore...configuration entry may not be present
            }

            if (maxSize > 0 && maxSize < (size / 1048576.00)) { // a megabyte
                throw new ItemExportException(ItemExportException.EXPORT_TOO_LARGE,
                                              "The overall size of this export is too large.  Please contact your " +
                                                  "administrator for more information.");
            }
        }

        // if we have any items to process then kick off anonymous thread
        if (itemsMap.size() > 0) {
            Thread go = new Thread() {
                @Override
                public void run() {
                    Context context = new Context();
                    Iterator<Item> iitems = null;
                    try {
                        // ignore auths
                        context.turnOffAuthorisationSystem();

                        String fileName = assembleFileName("item", eperson,
                                                           new Date());
                        String workParentDir = getExportWorkDirectory()
                            + System.getProperty("file.separator")
                            + fileName;
                        String downloadDir = getExportDownloadDirectory(eperson);
                        File dnDir = new File(downloadDir);
                        if (!dnDir.exists() && !dnDir.mkdirs()) {
                            logError("Unable to create download directory");
                        }

                        Iterator<String> iter = itemsMap.keySet().iterator();
                        while (iter.hasNext()) {
                            String keyName = iter.next();
                            List<UUID> uuids = itemsMap.get(keyName);
                            List<Item> items = new ArrayList<>();
                            for (UUID uuid : uuids) {
                                items.add(itemService.find(context, uuid));
                            }
                            iitems = items.iterator();

                            String workDir = workParentDir
                                + System.getProperty("file.separator")
                                + keyName;

                            File wkDir = new File(workDir);
                            if (!wkDir.exists() && !wkDir.mkdirs()) {
                                logError("Unable to create working directory");
                            }


                            // export the items using normal export method
                            exportItem(context, iitems, workDir, 1, migrate, false);
                        }

                        // now zip up the export directory created above
                        zip(workParentDir, downloadDir
                            + System.getProperty("file.separator")
                            + fileName + ".zip");
                        // email message letting user know the file is ready for
                        // download
                        emailSuccessMessage(context, eperson, fileName + ".zip");
                        // return to enforcing auths
                        context.restoreAuthSystemState();
                    } catch (Exception e1) {
                        try {
                            emailErrorMessage(eperson, e1.getMessage());
                        } catch (Exception e) {
                            // wont throw here
                        }
                        throw new IllegalStateException(e1);
                    } finally {
                        // Make sure the database connection gets closed in all conditions.
                        try {
                            context.complete();
                        } catch (SQLException sqle) {
                            context.abort();
                        }
                    }
                }

            };

            go.isDaemon();
            go.start();
        } else {
            Locale supportedLocale = I18nUtil.getEPersonLocale(eperson);
            emailErrorMessage(eperson, I18nUtil.getMessage("org.dspace.app.itemexport.no-result", supportedLocale));
        }
    }

    @Override
    public String assembleFileName(String type, EPerson eperson,
                                   Date date) throws Exception {
        // to format the date
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy_MMM_dd");
        String downloadDir = getExportDownloadDirectory(eperson);
        // used to avoid name collision
        int count = 1;
        boolean exists = true;
        String fileName = null;
        while (exists) {
            fileName = type + "_export_" + sdf.format(date) + "_" + count + "_"
                + eperson.getID();
            exists = new File(downloadDir
                                  + System.getProperty("file.separator") + fileName + ".zip")
                .exists();
            count++;
        }
        return fileName;
    }

    @Override
    public String getExportDownloadDirectory(EPerson ePerson)
        throws Exception {
        String downloadDir = configurationService
            .getProperty("org.dspace.app.itemexport.download.dir");
        if (downloadDir == null) {
            throw new Exception(
                "A dspace.cfg entry for 'org.dspace.app.itemexport.download.dir' does not exist.");
        }
        File result = new File(downloadDir + System.getProperty("file.separator") + ePerson.getID());
        if (!result.exists() && ePerson.getLegacyId() != null) {
            //Check for the old identifier
            result = new File(downloadDir + System.getProperty("file.separator") + ePerson.getLegacyId());
        }

        return result.getAbsolutePath();
    }

    @Override
    public String getExportWorkDirectory() throws Exception {
        String exportDir = configurationService
            .getProperty("org.dspace.app.itemexport.work.dir");
        if (exportDir == null) {
            throw new Exception(
                "A dspace.cfg entry for 'org.dspace.app.itemexport.work.dir' does not exist.");
        }
        // clean work dir path from duplicate separators
        return StringUtils.replace(exportDir, File.separator + File.separator, File.separator);
    }

    @Override
    public InputStream getExportDownloadInputStream(String fileName,
                                                    EPerson eperson) throws Exception {
        File file = new File(getExportDownloadDirectory(eperson)
                                 + System.getProperty("file.separator") + fileName);
        if (file.exists()) {
            return new FileInputStream(file);
        } else {
            return null;
        }
    }

    @Override
    public long getExportFileSize(Context context, String fileName) throws Exception {
        String strID = fileName.substring(fileName.lastIndexOf('_') + 1,
                                          fileName.lastIndexOf('.'));
        EPerson ePerson = getEPersonFromString(context, strID);


        File file = new File(
            getExportDownloadDirectory(ePerson)
                + System.getProperty("file.separator") + fileName);
        if (!file.exists() || !file.isFile()) {
            throw new FileNotFoundException("The file "
                                                + getExportDownloadDirectory(ePerson)
                                                + System.getProperty("file.separator") + fileName
                                                + " does not exist.");
        }

        return file.length();
    }

    /**
     * Attempt to find an EPerson based on string ID
     *
     * @param context DSpace context
     * @param strID   string identifier
     * @return EPerson object (if found)
     * @throws SQLException if database error
     */
    protected EPerson getEPersonFromString(Context context, String strID) throws SQLException {
        EPerson eperson;
        try {
            UUID ePersonId = UUID.fromString(strID);
            eperson = ePersonService.find(context, ePersonId);
        } catch (Exception e) {
            eperson = ePersonService.findByLegacyId(context, Integer.parseInt(strID));
        }
        return eperson;
    }

    @Override
    public long getExportFileLastModified(Context context, String fileName)
        throws Exception {
        String strID = fileName.substring(fileName.lastIndexOf('_') + 1,
                                          fileName.lastIndexOf('.'));
        EPerson ePerson = getEPersonFromString(context, strID);

        File file = new File(
            getExportDownloadDirectory(ePerson)
                + System.getProperty("file.separator") + fileName);
        if (!file.exists() || !file.isFile()) {
            throw new FileNotFoundException("The file "
                                                + getExportDownloadDirectory(ePerson)
                                                + System.getProperty("file.separator") + fileName
                                                + " does not exist.");
        }

        return file.lastModified();
    }

    @Override
    public boolean canDownload(Context context, String fileName) {
        EPerson eperson = context.getCurrentUser();
        if (eperson == null) {
            return false;
        }
        String strID = fileName.substring(fileName.lastIndexOf('_') + 1,
                                          fileName.lastIndexOf('.'));
        try {
            if (strID.equals(eperson.getID().toString())) {
                return true;
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    @Override
    public List<String> getExportsAvailable(EPerson eperson)
        throws Exception {
        File downloadDir = new File(getExportDownloadDirectory(eperson));
        if (!downloadDir.exists() || !downloadDir.isDirectory()) {
            return null;
        }

        List<String> fileNames = new ArrayList<>();

        for (String fileName : downloadDir.list()) {
            if (fileName.contains("export") && fileName.endsWith(".zip")) {
                fileNames.add(fileName);
            }
        }

        if (fileNames.size() > 0) {
            return fileNames;
        }

        return null;
    }

    @Override
    public void deleteOldExportArchives(EPerson eperson) throws Exception {
        int hours = configurationService
            .getIntProperty("org.dspace.app.itemexport.life.span.hours");
        Calendar now = Calendar.getInstance();
        now.setTime(new Date());
        now.add(Calendar.HOUR, -hours);
        File downloadDir = new File(getExportDownloadDirectory(eperson));
        if (downloadDir.exists()) {
            File[] files = downloadDir.listFiles();
            for (File file : files) {
                if (file.lastModified() < now.getTimeInMillis()) {
                    if (!file.delete()) {
                        logError("Unable to delete export file");
                    }
                }
            }
        }

    }

    @Override
    public void deleteOldExportArchives() throws Exception {
        int hours = configurationService.getIntProperty("org.dspace.app.itemexport.life.span.hours");
        Calendar now = Calendar.getInstance();
        now.setTime(new Date());
        now.add(Calendar.HOUR, -hours);
        File downloadDir = new File(configurationService.getProperty("org.dspace.app.itemexport.download.dir"));
        if (downloadDir.exists()) {
            // Get a list of all the sub-directories, potentially one for each ePerson.
            File[] dirs = downloadDir.listFiles();
            for (File dir : dirs) {
                // For each sub-directory delete any old files.
                File[] files = dir.listFiles();
                for (File file : files) {
                    if (file.lastModified() < now.getTimeInMillis()) {
                        if (!file.delete()) {
                            logError("Unable to delete old files");
                        }
                    }
                }

                // If the directory is now empty then we delete it too.
                if (dir.listFiles().length == 0) {
                    if (!dir.delete()) {
                        logError("Unable to delete directory");
                    }
                }
            }
        }

    }


    @Override
    public void emailSuccessMessage(Context context, EPerson eperson,
                                    String fileName) throws MessagingException {
        try {
            Locale supportedLocale = I18nUtil.getEPersonLocale(eperson);
            Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "export_success"));
            email.addRecipient(eperson.getEmail());
            email.addArgument(configurationService.getProperty("dspace.ui.url") + "/exportdownload/" + fileName);
            email.addArgument(configurationService.getProperty("org.dspace.app.itemexport.life.span.hours"));

            email.send();
        } catch (Exception e) {
            logWarn(LogHelper.getHeader(context, "emailSuccessMessage", "cannot notify user of export"), e);
        }
    }

    @Override
    public void emailErrorMessage(EPerson eperson, String error)
        throws MessagingException {
        logWarn("An error occurred during item export, the user will be notified. " + error);
        try {
            Locale supportedLocale = I18nUtil.getEPersonLocale(eperson);
            Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "export_error"));
            email.addRecipient(eperson.getEmail());
            email.addArgument(error);
            email.addArgument(configurationService.getProperty("dspace.ui.url") + "/feedback");

            email.send();
        } catch (Exception e) {
            logWarn("error during item export error notification", e);
        }
    }

    @Override
    public void zip(String strSource, String target) throws Exception {
        ZipOutputStream cpZipOutputStream = null;
        String tempFileName = target + "_tmp";
        try {
            File cpFile = new File(strSource);
            if (!cpFile.isFile() && !cpFile.isDirectory()) {
                return;
            }
            File targetFile = new File(tempFileName);
            if (!targetFile.createNewFile()) {
                logWarn("Target file already exists: " + targetFile.getName());
            }

            FileOutputStream fos = new FileOutputStream(tempFileName);
            cpZipOutputStream = new ZipOutputStream(fos);
            cpZipOutputStream.setLevel(9);
            zipFiles(cpFile, strSource, tempFileName, cpZipOutputStream);
            cpZipOutputStream.finish();
            cpZipOutputStream.close();
            cpZipOutputStream = null;

            // Fix issue on Windows with stale file handles open before trying to delete them
            System.gc();

            deleteDirectory(cpFile);
            if (!targetFile.renameTo(new File(target))) {
                logError("Unable to rename file");
            }
        } finally {
            if (cpZipOutputStream != null) {
                cpZipOutputStream.close();
            }
        }
    }

    /**
     * @param cpFile            file
     * @param strSource         source location
     * @param strTarget         target location
     * @param cpZipOutputStream current zip outputstream
     * @throws Exception if error
     */
    protected void zipFiles(File cpFile, String strSource,
                            String strTarget, ZipOutputStream cpZipOutputStream)
        throws Exception {
        int byteCount;
        final int DATA_BLOCK_SIZE = 2048;
        FileInputStream cpFileInputStream = null;
        if (cpFile.isDirectory()) {
            File[] fList = cpFile.listFiles();
            for (File aFList : fList) {
                zipFiles(aFList, strSource, strTarget, cpZipOutputStream);
            }
        } else {
            try {
                if (cpFile.getAbsolutePath().equalsIgnoreCase(strTarget)) {
                    return;
                }
                String strAbsPath = cpFile.getPath();
                int startIndex = strSource.length();
                if (!StringUtils.endsWith(strSource, File.separator)) {
                    startIndex++;
                }
                String strZipEntryName = strAbsPath.substring(startIndex, strAbsPath.length());

                // byte[] b = new byte[ (int)(cpFile.length()) ];

                cpFileInputStream = new FileInputStream(cpFile);

                ZipEntry cpZipEntry = new ZipEntry(strZipEntryName);
                cpZipOutputStream.putNextEntry(cpZipEntry);

                byte[] b = new byte[DATA_BLOCK_SIZE];
                while ((byteCount = cpFileInputStream.read(b, 0,
                                                           DATA_BLOCK_SIZE)) != -1) {
                    cpZipOutputStream.write(b, 0, byteCount);
                }

                // cpZipOutputStream.write(b, 0, (int)cpFile.length());
            } finally {
                if (cpFileInputStream != null) {
                    cpFileInputStream.close();
                }
                cpZipOutputStream.closeEntry();
            }
        }
    }

    /**
     * Delete a directory
     *
     * @param path directory path
     * @return true if successful, false otherwise
     */
    protected boolean deleteDirectory(File path) {
        if (path.exists()) {
            File[] files = path.listFiles();
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteDirectory(file);
                } else {
                    if (!file.delete()) {
                        logError("Unable to delete file: " + file.getName());
                    }
                }
            }
        }

        return (path.delete());
    }

    @Override
    public void setHandler(DSpaceRunnableHandler handler) {
        this.handler = handler;
    }

    private void logInfo(String message) {
        logInfo(message, null);
    }

    private void logInfo(String message, Exception e) {
        if (handler != null) {
            handler.logInfo(message);
            return;
        }

        if (e != null) {
            log.info(message, e);
        } else {
            log.info(message);
        }
    }

    private void logWarn(String message) {
        logWarn(message, null);
    }

    private void logWarn(String message, Exception e) {
        if (handler != null) {
            handler.logWarning(message);
            return;
        }

        if (e != null) {
            log.warn(message, e);
        } else {
            log.warn(message);
        }
    }

    private void logError(String message) {
        logError(message, null);
    }

    private void logError(String message, Exception e) {
        if (handler != null) {
            if (e != null) {
                handler.logError(message, e);
            } else {
                handler.logError(message);
            }
            return;
        }

        if (e != null) {
            log.error(message, e);
        } else {
            log.error(message);
        }
    }

}
