org.codice.ddf.configuration.migration.ImportMigrationEntryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.configuration.migration.ImportMigrationEntryImpl.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This is free software: you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation, either version 3 of
 * the License, or any later version.
 *
 * <p>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 Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.configuration.migration;

import com.google.common.annotations.VisibleForTesting;
import ddf.security.common.audit.SecurityLogger;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.Validate;
import org.codice.ddf.configuration.migration.util.AccessUtils;
import org.codice.ddf.migration.ImportMigrationEntry;
import org.codice.ddf.migration.MigrationException;
import org.codice.ddf.migration.MigrationReport;
import org.codice.ddf.migration.MigrationWarning;
import org.codice.ddf.util.function.BiThrowingConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** This class provides an implementation of the {@link ImportMigrationEntry} interface. */
public class ImportMigrationEntryImpl extends MigrationEntryImpl implements ImportMigrationEntry {

    private static final Logger LOGGER = LoggerFactory.getLogger(ImportMigrationEntryImpl.class);

    private static final String IMPORTING = "Importing {}...";

    private final Map<String, ImportMigrationJavaPropertyReferencedEntryImpl> properties = new HashMap<>(8);

    private final ImportMigrationContextImpl context;

    private final Path absolutePath;

    private final Path path;

    private final File file;

    private final String name;

    @Nullable
    private final ZipEntry entry;

    private final boolean isFile;

    /**
     * Will track if restore was attempted along with its result. Will be <code>null</code> until
     * restore() is attempted, at which point it will start tracking the first restore() result.
     */
    protected Boolean restored = null;

    /**
     * Instantiates a new migration entry by parsing the provided zip entry's name for a migratable
     * identifier and an entry relative name.
     *
     * @param contextProvider a provider for migration contexts given a migratable id
     * @param ze the zip entry for which we are creating an entry
     */
    ImportMigrationEntryImpl(Function<String, ImportMigrationContextImpl> contextProvider, ZipEntry ze) {
        // we still must sanitize because there could be a mix of / and \ and Paths.get() doesn't
        // support that
        final Path fqn = Paths.get(FilenameUtils.separatorsToSystem(ze.getName()));
        final int count = fqn.getNameCount();

        if (count > 1) {
            this.context = contextProvider.apply(fqn.getName(0).toString());
            this.path = fqn.subpath(1, count);
        } else { // system entry
            this.context = contextProvider.apply(null);
            this.path = fqn;
        }
        this.absolutePath = context.getPathUtils().resolveAgainstDDFHome(path);
        this.file = absolutePath.toFile();
        this.name = FilenameUtils.separatorsToUnix(path.toString());
        this.entry = ze;
        this.isFile = true;
    }

    /**
     * Instantiates a new migration entry with the given name.
     *
     * @param context the migration context associated with this entry
     * @param name the entry's relative name
     * @param isFile <code>true</code> if the entry represents a file; <code>false</code> if it
     *     represents a directory
     */
    protected ImportMigrationEntryImpl(ImportMigrationContextImpl context, String name, boolean isFile) {
        this.context = context;
        this.path = Paths.get(name);
        this.name = FilenameUtils.separatorsToUnix(name);
        this.absolutePath = context.getPathUtils().resolveAgainstDDFHome(path);
        this.file = absolutePath.toFile();
        this.entry = null;
        this.isFile = isFile;
    }

    /**
     * Instantiates a new migration entry with the given path.
     *
     * @param context the migration context associated with this entry
     * @param path the entry's relative path
     * @param isFile <code>true</code> if the entry represents a file; <code>false</code> if it
     *     represents a directory
     */
    protected ImportMigrationEntryImpl(ImportMigrationContextImpl context, Path path, boolean isFile) {
        this.context = context;
        this.path = path;
        this.name = FilenameUtils.separatorsToUnix(path.toString());
        this.absolutePath = context.getPathUtils().resolveAgainstDDFHome(path);
        this.file = absolutePath.toFile();
        this.entry = null;
        this.isFile = isFile;
    }

    @Override
    public MigrationReport getReport() {
        return context.getReport();
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Path getPath() {
        return path;
    }

    @Override
    public long getLastModifiedTime() {
        return (entry != null) ? entry.getTime() : -1L;
    }

    @Override
    public boolean isDirectory() {
        return !isFile;
    }

    @Override
    public boolean isFile() {
        return isFile;
    }

    @Override
    public final Optional<InputStream> getInputStream() throws IOException {
        return getInputStream(true);
    }

    @Override
    public boolean restore(boolean required) {
        if (restored == null) {
            this.restored = false; // until proven otherwise
            this.restored = handleRestore(required, null);
        }
        return restored;
    }

    @Override
    public boolean restore(boolean required, PathMatcher filter) {
        Validate.notNull(filter, "invalid null path filter");
        if (restored == null) {
            this.restored = false; // until proven otherwise
            if (filter.matches(path)) {
                this.restored = handleRestore(required, filter);
            } else {
                this.restored = handleRestoreWhenFilterNotMatching(required);
            }
        }
        return restored;
    }

    @Override
    public boolean restore(BiThrowingConsumer<MigrationReport, Optional<InputStream>, IOException> consumer) {
        Validate.notNull(consumer, "invalid null consumer");
        if (restored == null) {
            this.restored = false; // until proven otherwise
            Optional<InputStream> is = Optional.empty();

            try {
                is = getInputStream(true);
                final Optional<InputStream> fis = is;

                this.restored = getReport().wasIOSuccessful(() -> consumer.accept(getReport(), fis));
            } catch (IOException e) {
                getReport().record(new MigrationException(Messages.IMPORT_PATH_COPY_ERROR, path,
                        context.getPathUtils().getDDFHome(), e));
            } finally {
                is.ifPresent(IOUtils::closeQuietly); // we do not care if we cannot close it
            }
        }
        return restored;
    }

    @Override
    public Optional<ImportMigrationEntry> getPropertyReferencedEntry(String pname) {
        Validate.notNull(pname, "invalid null property name");
        return Optional.ofNullable(properties.get(pname));
    }

    @Override
    protected ImportMigrationContextImpl getContext() {
        return context;
    }

    protected Path getAbsolutePath() {
        return absolutePath;
    }

    protected File getFile() {
        return file;
    }

    protected Optional<InputStream> getInputStream(boolean checkAccess) throws IOException {
        return Optional.ofNullable(context.getInputStreamFor(this, checkAccess));
    }

    /**
     * Gets a debug string to represent this entry.
     *
     * @return a debug string for this entry
     */
    protected String toDebugString() {
        return String.format("file [%s] from [%s]", absolutePath, path);
    }

    @SuppressWarnings("PMD.DefaultPackage" /* designed to be called from ImportMigrationContextImpl within this package */)
    @VisibleForTesting
    void addPropertyReferenceEntry(String pname, ImportMigrationJavaPropertyReferencedEntryImpl entry) {
        properties.put(pname, entry);
    }

    @VisibleForTesting
    Map<String, ImportMigrationJavaPropertyReferencedEntryImpl> getJavaPropertyReferencedEntries() {
        return properties;
    }

    @SuppressWarnings("PMD.DefaultPackage" /* designed to be called from ImportMigrationContextImpl within this package */)
    @Nullable
    ZipEntry getZipEntry() {
        return entry;
    }

    @VisibleForTesting
    boolean isMigratable() {
        final MigrationReport report = getContext().getReport();

        if (path.isAbsolute()) {
            report.record(new MigrationWarning(Messages.IMPORT_OPTIONAL_PATH_DELETE_WARNING, path,
                    String.format("is outside [%s]", getContext().getPathUtils().getDDFHome())));
            return false;
        } else if (AccessUtils.doPrivileged(() -> Files.isSymbolicLink(getAbsolutePath()))) {
            report.record(
                    new MigrationWarning(Messages.IMPORT_OPTIONAL_PATH_DELETE_WARNING, path, "is a symbolic link"));
            return false;
        }
        return true;
    }

    private boolean handleRestore(boolean required, @Nullable PathMatcher filter) {
        InputStream is = null;

        try {
            is = getInputStream(false).orElse(null);
            final InputStream fis = is;

            // if the file was exported by the framework then no need to check any permissions and we
            // can simply execute with our own privileges
            // but if the file was not exported by the framework then we want to make sure the
            // migratable has the right to do what we will be doing; so let's make sure to not extend
            // our privileges
            return AccessUtils.doConditionallyPrivileged(!context.requiresWriteAccess(this),
                    () -> getReport().wasIOSuccessful(() -> handlePrivilegedRestore(required, filter, fis)));
        } catch (IOException e) {
            getReport().record(new MigrationException(Messages.IMPORT_PATH_COPY_ERROR, path,
                    context.getPathUtils().getDDFHome(), e));
            return false;
        } finally {
            IOUtils.closeQuietly(is); // we do not care if we cannot close it
        }
    }

    private void handlePrivilegedRestore(boolean required, @Nullable PathMatcher filter, @Nullable InputStream is)
            throws IOException {
        String debugString = null;

        if (LOGGER.isDebugEnabled()) {
            debugString = toDebugString();
            if (required) {
                debugString = "required " + debugString;
            }
            if (filter != null) {
                debugString += " with path filter";
            }
        }
        if (is == null) { // no entry stored!!!!
            handleRestoreWhenNoEntryWasExported(required, debugString);
        } else {
            handleRestoreWhenAnEntryWasExported(is, debugString);
        }
    }

    private boolean handleRestoreWhenFilterNotMatching(boolean required) {
        // we have a filter that doesn't match this entry, so treat it as if the file was not
        // there to start with
        if (required) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(ImportMigrationEntryImpl.IMPORTING, toDebugString());
            }
            getReport().record(new MigrationException(Messages.IMPORT_PATH_COPY_ERROR, path,
                    context.getPathUtils().getDDFHome(), "it does not match filter"));
            return false;
        } // else - optional so no warnings/errors - just skip it and treat it as successful
        return true;
    }

    private void handleRestoreWhenNoEntryWasExported(boolean required, @Nullable String debugString) {
        if (required) {
            LOGGER.debug(ImportMigrationEntryImpl.IMPORTING, debugString);
            getReport().record(new MigrationException(Messages.IMPORT_PATH_NOT_EXPORTED_ERROR, path));
        } else {
            // it is optional so delete it as it was optional when we exported and wasn't on
            // disk so we want to make sure we end up without the file on disk after import
            if (isMigratable() && file.exists()) { // but only if it is migratable and exist to start with
                LOGGER.debug("Deleting {}...", debugString);
                // WAIT!! that could be a problem if the migratable ask for files it did not
                // export!!!!!!!!!!
                if (file.delete()) {
                    SecurityLogger.audit("Deleted file {}", file);
                } else {
                    SecurityLogger.audit("Error deleting file {}", file);
                    getReport().record(new MigrationException(Messages.IMPORT_OPTIONAL_PATH_DELETE_ERROR, path));
                }
            }
        }
    }

    private void restoreLastModifiedTime() {
        final long modified = getLastModifiedTime();

        if ((modified != -1L) && !file.setLastModified(getLastModifiedTime())) { // propagate last modified time
            LOGGER.debug("Failed to reset last modified time for {} to {}", getAbsolutePath(), modified);
        }
    }

    private void handleRestoreWhenAnEntryWasExported(InputStream is, @Nullable String debugString)
            throws IOException {
        LOGGER.debug(ImportMigrationEntryImpl.IMPORTING, debugString);
        try {
            FileUtils.copyInputStreamToFile(is, file);
            restoreLastModifiedTime();
            SecurityLogger.audit("Imported file {}", file);
        } catch (IOException e) {
            if (!file.canWrite()) { // make it writable and try again
                if (!retryHandleRestoreWhenAnEntryWasExported(context.getInputStreamFor(this, false))) {
                    throw e;
                }
            } else {
                SecurityLogger.audit("Error importing file {}", file);
                throw e;
            }
        }
    }

    // Meant to be called when the file is not writeable.
    private boolean retryHandleRestoreWhenAnEntryWasExported(InputStream is) throws IOException {
        LOGGER.debug("temporarily overriding write privileges for {}", file);
        if (file.setWritable(true)) {
            SecurityLogger.audit("Enabled write privileges for file {}", file);
        } else { // cannot set it writable so bail
            SecurityLogger.audit("Error enabling write privileges for file {}", file);
            return false;
        }
        try {
            FileUtils.copyInputStreamToFile(is, file);
            restoreLastModifiedTime();
            SecurityLogger.audit("Imported file {}", file);
            return true;
        } catch (IOException ee) {
            SecurityLogger.audit("Error importing file {}", file);
            throw ee;
        } finally {
            IOUtils.closeQuietly(is); // we do not care if we cannot close it
            if (file.setWritable(false)) { // reset the permissions properly
                SecurityLogger.audit("Disabled write privileges for file {}", file);
            } else {
                SecurityLogger.audit("Error disabling write privileges for file {}", file);
            }
        }
    }

    /**
     * The superclass implementation is sufficient for our needs.
     *
     * @param o the object to check
     * @return true if equal
     */
    @Override
    public boolean equals(Object o) {
        return super.equals(o);
    }

    /**
     * The superclass implementation is sufficient for our needs.
     *
     * @return the hashcode
     */
    @Override
    public int hashCode() {
        return super.hashCode();
    }
}