org.sigmah.server.file.impl.BackupArchiveJob.java Source code

Java tutorial

Introduction

Here is the source code for org.sigmah.server.file.impl.BackupArchiveJob.java

Source

package org.sigmah.server.file.impl;

/*
 * #%L
 * Sigmah
 * %%
 * Copyright (C) 2010 - 2016 URD
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.Deflater;
import java.util.zip.ZipOutputStream;

import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.sigmah.server.dao.FileDAO;
import org.sigmah.server.dao.OrgUnitDAO;
import org.sigmah.server.dao.ProjectDAO;
import org.sigmah.server.dao.UserDAO;
import org.sigmah.server.dao.ValueDAO;
import org.sigmah.server.domain.OrgUnit;
import org.sigmah.server.domain.Project;
import org.sigmah.server.domain.User;
import org.sigmah.server.domain.element.FilesListElement;
import org.sigmah.server.domain.value.FileVersion;
import org.sigmah.server.domain.value.Value;
import org.sigmah.server.file.FileStorageProvider;
import org.sigmah.server.file.util.FileElement;
import org.sigmah.server.file.util.FolderElement;
import org.sigmah.server.file.util.RepositoryElement;
import org.sigmah.server.handler.util.Handlers;
import org.sigmah.server.servlet.util.ResponseHelper;
import org.sigmah.shared.dto.BackupDTO;
import org.sigmah.shared.dto.value.FileDTO.LoadingScope;
import org.sigmah.shared.util.ValueResultUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.persist.Transactional;

/**
 * <p>
 * Runnable job in charge of generating an organization backup archive.
 * </p>
 * <p>
 * As this process may take a while, the job is executed in a parallel thread.
 * </p>
 * 
 * @author Denis Colliot (dcolliot@ideia.fr)
 */
public class BackupArchiveJob implements Runnable {

    /**
     * Backup archive job argument POJO.
     * 
     * @author Denis Colliot (dcolliot@ideia.fr)
     */
    public static final class BackupArchiveJobArgument {

        private BackupDTO backup;
        private Integer userId;
        private Path tempArchiveFile;
        private Path finalArchiveFile;

        BackupArchiveJobArgument(final BackupDTO backup, final Integer userId, final Path tempArchiveFile,
                final Path finalArchiveFile) {
            this.backup = backup;
            this.userId = userId;
            this.tempArchiveFile = tempArchiveFile;
            this.finalArchiveFile = finalArchiveFile;
        }
    }

    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(BackupArchiveJob.class);

    /**
     * Assisted injected job arguments.
     */
    private final BackupArchiveJobArgument arguments;

    /**
     * Injected {@link ProjectDAO}.
     */
    @Inject
    private ProjectDAO projectDAO;

    /**
     * Injected {@link FileDAO}.
     */
    @Inject
    private FileDAO fileDAO;

    /**
     * Injected {@link ValueDAO}.
     */
    @Inject
    private ValueDAO valueDAO;

    /**
     * Injected {@link OrgUnitDAO}.
     */
    @Inject
    private OrgUnitDAO orgUnitDAO;

    /**
     * Injected {@link UserDAO}.
     */
    @Inject
    private UserDAO userDAO;

    /**
     * Injected {@link FileStorageProvider}.
     */
    @Inject
    private FileStorageProvider fileStorageProvider;

    @Inject
    public BackupArchiveJob(@Assisted final BackupArchiveJobArgument arguments) {
        this.arguments = arguments;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run() {

        final Path tempArchiveFile = arguments.tempArchiveFile;
        final Path finalArchiveFile = arguments.finalArchiveFile;

        try (final ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(
                Files.newOutputStream(tempArchiveFile))) {

            zipOutputStream.setMethod(ZipOutputStream.DEFLATED);
            zipOutputStream.setLevel(Deflater.BEST_COMPRESSION);

            final RepositoryElement repository = buildOrgUnitRepository(arguments.backup, arguments.userId);
            repository.setName("");

            zipRepository(repository, zipOutputStream, "");

            // TODO Delete existing previous organization file(s).

            // Renames temporary '.tmp' file to complete '.zip' file.
            Files.move(tempArchiveFile, finalArchiveFile, StandardCopyOption.REPLACE_EXISTING);

        } catch (final Throwable t) {

            if (LOG.isErrorEnabled()) {
                LOG.error("An error occurred during backup archive generation process.", t);
            }

            try {

                Files.deleteIfExists(tempArchiveFile);
                Files.deleteIfExists(finalArchiveFile);

            } catch (final IOException e) {
                if (LOG.isErrorEnabled()) {
                    LOG.error("An error occurred while deleting archive error file.", e);
                }
            }
        }
    }

    // --------------------------------------------------------------------------------------------------------------
    //
    // UTILITY METHODS.
    //
    // --------------------------------------------------------------------------------------------------------------

    /**
     * Builds the given {@code orgUnit} corresponding files repository tree.
     * 
     * @param backup
     *          The backup configuration.
     * @param userId
     *          The id of the user requesting the repository build.
     * @return The given {@code orgUnit} corresponding files repository tree, or {@code null} if the given {@code user} is
     *         not authorized to access the given {@code backup} corresponding OrgUnit.
     * @throws IllegalArgumentException
     *           If one of the arguments is missing.
     * @throws UnsupportedOperationException
     *           If the OrgUnit is not visible to the user.
     */
    @Transactional
    // IMPORTANT: '@Transactional' annotation ensures proper database connections closure.
    protected RepositoryElement buildOrgUnitRepository(final BackupDTO backup, final Integer userId) {

        if (backup == null || userId == null) {
            throw new IllegalArgumentException("Invalid arguments necessary to build OrgUnit files repository.");
        }

        final LoadingScope loadingScope = backup.getLoadingScope();
        final OrgUnit orgUnit = orgUnitDAO.findById(backup.getOrgUnitId());
        final User user = userDAO.findById(userId);

        // --
        // Controls.
        // --

        if (orgUnit == null) {
            throw new IllegalArgumentException("Cannot find OrgUnit with id #" + backup.getOrgUnitId() + ".");
        }

        if (user == null) {
            throw new IllegalArgumentException("Cannot find User with id #" + userId + ".");
        }

        if (!Handlers.isOrgUnitVisible(orgUnit, user)) {
            throw new UnsupportedOperationException(
                    "OrgUnit #" + orgUnit.getId() + " is not visible to user #" + userId + ".");
        }

        // --
        // Starts repository building.
        // --

        final FolderElement root = new FolderElement("root", "root");

        final Set<OrgUnit> orgUnitTree = new HashSet<OrgUnit>();
        Handlers.crawlUnits(orgUnit, orgUnitTree, true);

        // Retrieves values for OrgUnits full tree.
        final List<Value> values = valueDAO.findValuesForOrgUnits(orgUnitTree);

        for (final Value value : values) {

            if (!(value.getElement() instanceof FilesListElement)) {
                // Not a file element.
                continue;
            }

            final Project ud = projectDAO.findById(value.getContainerId());
            final OrgUnit o;

            if (ud != null) {
                // There is only one partner.
                o = ud.getPartners().iterator().next();

            } else {
                // Container is an OrgUnit.
                o = orgUnitDAO.findById(value.getContainerId());
            }

            final List<FileVersion> versions = fileDAO
                    .findVersions(ValueResultUtils.splitValuesAsInteger(value.getValue()), loadingScope);

            FolderElement orgUnitRepository = (FolderElement) root.getById("o" + o.getId());
            if (orgUnitRepository == null) {
                orgUnitRepository = new FolderElement("o" + o.getId(), validateFileName(o.getFullName()));
                root.appendChild(orgUnitRepository);
            }

            FolderElement fileFolderElementParent = null;

            if (ud != null) {
                fileFolderElementParent = (FolderElement) orgUnitRepository.getById("p" + ud.getId());
                if (fileFolderElementParent == null) {
                    fileFolderElementParent = new FolderElement("p" + ud.getId(),
                            validateFileName(ud.getFullName()));
                    orgUnitRepository.appendChild(fileFolderElementParent);
                }
            } else {
                fileFolderElementParent = orgUnitRepository;
            }

            for (final FileVersion version : versions) {

                if (loadingScope == LoadingScope.ALL_VERSIONS) {

                    FolderElement fileRepository = (FolderElement) fileFolderElementParent
                            .getById("f" + version.getParentFile().getId());

                    if (fileRepository == null) {
                        fileRepository = new FolderElement("f" + version.getParentFile().getId(),
                                validateFileName(version.getParentFile().getName()) + "_f"
                                        + version.getParentFile().getId());
                        fileFolderElementParent.appendChild(fileRepository);
                    }

                    final FileElement file = new FileElement("fv" + version.getId(),
                            validateFileName(version.getName()) + "_v" + version.getId() + "."
                                    + version.getExtension(),
                            version.getPath());
                    fileRepository.appendChild(file);

                } else if (loadingScope == LoadingScope.LAST_VERSION) {

                    final FileElement file = new FileElement("f" + version.getId(),
                            validateFileName(version.getName()) + "_f" + version.getId() + "."
                                    + version.getExtension(),
                            version.getPath());
                    fileFolderElementParent.appendChild(file);
                }
            }
        }

        return root;
    }

    /**
     * <p>
     * Recursively browses the given {@code root} repository elements to populate the given {@code zipOutputStream} with
     * corresponding files.
     * </p>
     * <p>
     * If a referenced file cannot be found in the storage folder, it will be ignored (a {@code WARN} log is generated).
     * </p>
     * 
     * @param root
     *          The root repository element.
     * @param zipOutputStream
     *          The stream to populate with files hierarchy.
     * @param actualPath
     *          The current repository path.
     */
    private void zipRepository(final RepositoryElement root, final ZipArchiveOutputStream zipOutputStream,
            final String actualPath) {

        final String path = (actualPath.equals("") ? root.getName() : actualPath + "/" + root.getName());

        if (root instanceof FileElement) {

            final FileElement file = (FileElement) root;
            final String fileStorageId = file.getStorageId();

            if (fileStorageProvider.exists(fileStorageId)) {
                try (final InputStream is = new BufferedInputStream(fileStorageProvider.open(fileStorageId),
                        ResponseHelper.BUFFER_SIZE)) {

                    zipOutputStream.putArchiveEntry(new ZipArchiveEntry(path));

                    final byte data[] = new byte[ResponseHelper.BUFFER_SIZE];

                    while ((is.read(data)) != -1) {
                        zipOutputStream.write(data);
                    }

                    zipOutputStream.closeArchiveEntry();

                } catch (final IOException e) {
                    LOG.warn("File '" + fileStorageId + "' cannot be found ; continuing with next file.", e);
                }
            } else {
                LOG.warn("File '{0}' does not exists on the server ; continuing with next file.", fileStorageId);
            }

        } else if (root instanceof FolderElement) {

            final FolderElement folder = (FolderElement) root;

            for (final RepositoryElement element : folder.getChildren()) {
                zipRepository(element, zipOutputStream, path);
            }
        }
    }

    /**
     * Deletes the {@code "C:\fakepath\"} string from given {@code fileName} (which comes from an issue in Google Chrome).
     * It also replaces all wrong characters that can't be displayed in a file name or a directory name by "{@code _}".
     * 
     * @param fileName
     *          The file name to validate.
     * @return The validated file name.
     */
    private static String validateFileName(final String fileName) {
        return fileName.replaceFirst("[cC]:\\\\fakepath\\\\", "").replaceAll("[\\/:*?\"<>|]", "_");
    }

}