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

Java tutorial

Introduction

Here is the source code for org.codice.ddf.configuration.migration.ExportMigrationEntryImpl.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 ddf.security.common.audit.SecurityLogger;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiPredicate;
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.ExportMigrationEntry;
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 ExportMigrationEntry}. */
public class ExportMigrationEntryImpl extends MigrationEntryImpl implements ExportMigrationEntry {

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

    private static final String FAILED_TO_BE_EXPORTED = "failed to be exported";

    private static final String ERROR_EXPORTING_FILE = "Error exporting file {}";

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

    private final AtomicReference<OutputStream> outputStream = new AtomicReference<>();

    private final ExportMigrationContextImpl context;

    private final Path absolutePath;

    private final Exception absolutePathError;

    private final Path path;

    private final File file;

    private final String name;

    private boolean isFile;

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

    /**
     * Instantiates a new migration entry given a migratable context and path.
     *
     * <p><i>Note:</i> In this version of the constructor, the path is either absolute or assumed to
     * be relative to ${ddf.home}. It will also be automatically relativized to ${ddf.home}.
     *
     * @param context the migration context associated with this entry
     * @param path the path for this entry
     * @throws IllegalArgumentException if <code>context</code> or <code>path</code> is <code>null
     * </code>
     */
    protected ExportMigrationEntryImpl(ExportMigrationContextImpl context, Path path) {
        Validate.notNull(context, "invalid null context");
        Validate.notNull(path, "invalid null path");
        Path apath;
        Exception aerror;

        try {
            // make sure it is resolved against ddf.home and not the current working directory
            apath = AccessUtils.doPrivileged(() -> {
                final Path p = context.getPathUtils().resolveAgainstDDFHome(path)
                        .toRealPath(LinkOption.NOFOLLOW_LINKS);

                this.isFile = p.toFile().isFile();
                return p;
            });
            aerror = null;
        } catch (IOException e) {
            apath = path;
            this.isFile = true; // since we can't find an absolute path on disk, we got to assume it's a file
            // remember the error in case the migratable attempts to store the file from disk later
            // instead of providing its own data
            aerror = e;
        }
        this.context = context;
        this.absolutePath = apath;
        this.absolutePathError = aerror;
        this.path = context.getPathUtils().relativizeFromDDFHome(apath);
        this.file = apath.toFile();
        // we keep the entry name in Unix style based on our convention
        this.name = FilenameUtils.separatorsToUnix(this.path.toString());
    }

    /**
     * Instantiates a new migration entry given a migratable context and path.
     *
     * <p><i>Note:</i> In this version of the constructor, the path is either absolute or assumed to
     * be relative to ${ddf.home}. It will also be automatically relativized to ${ddf.home}.
     *
     * @param context the migration context associated with this entry
     * @param pathname the path string for this entry
     * @throws IllegalArgumentException if <code>context</code> or <code>pathname</code> is <code>null
     * </code>
     */
    protected ExportMigrationEntryImpl(ExportMigrationContextImpl context, String pathname) {
        this(context, Paths.get(ExportMigrationEntryImpl.validateNotNull(pathname, "invalid null pathname")));
    }

    private static <T> T validateNotNull(T t, String msg) {
        Validate.notNull(t, msg);
        return t;
    }

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

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

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

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

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

    @Override
    public long getLastModifiedTime() {
        return file.lastModified();
    }

    @Override
    public OutputStream getOutputStream() throws IOException {
        this.isFile = true; // force it to be represented as a file since the migratable will be providing the
        // data
        recordEntry();
        return getOutputStreamWithoutRecordingEntry();
    }

    @Override
    public boolean store(boolean required) {
        if (stored == null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Exporting {}{}...", (required ? "required " : ""), toDebugString());
            }
            this.stored = false; // until proven otherwise
            if (absolutePathError instanceof NoSuchFileException) {
                this.stored = handleStoreWhenNoSuchFile(required);
            } else if (absolutePathError != null) {
                SecurityLogger.audit(ExportMigrationEntryImpl.ERROR_EXPORTING_FILE, absolutePath);
                getReport().record(newError("cannot be read", absolutePathError));
            } else {
                this.stored = handleStoreWhenFileExist(null);
            }
        }
        return stored;
    }

    @Override
    public boolean store(boolean required, PathMatcher filter) {
        Validate.notNull(filter, "invalid null path filter");
        if (stored == null) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Exporting {}{} with path filter...", (required ? "required " : ""), toDebugString());
            }
            this.stored = false; // until proven otherwise
            if (isFile && !filter.matches(path)) {
                this.stored = handleStoreWhenFilterMismatch(required);
            } else if (absolutePathError instanceof NoSuchFileException) {
                this.stored = handleStoreWhenNoSuchFile(required);
            } else if (absolutePathError != null) {
                SecurityLogger.audit(ExportMigrationEntryImpl.ERROR_EXPORTING_FILE, absolutePath);
                getReport().record(newError("cannot be read", absolutePathError));
            } else {
                this.stored = handleStoreWhenFileExist(filter);
            }
        }
        return stored;
    }

    @Override
    public boolean store(BiThrowingConsumer<MigrationReport, OutputStream, IOException> consumer) {
        Validate.notNull(consumer, "invalid null consumer");
        if (stored == null) {
            this.stored = false; // until proven otherwise
            try (final OutputStream os = getOutputStream()) {
                this.stored = getReport().wasIOSuccessful(() -> consumer.accept(getReport(), os));
            } catch (ExportIOException e) {
                // special case indicating the I/O error occurred while writing to the zip which
                // would invalidate the zip so we are forced to abort
                throw newError(ExportMigrationEntryImpl.FAILED_TO_BE_EXPORTED, e.getCause());
            } catch (IOException e) {
                // here it means the error came out of reading/processing the input file/stream
                // where it is safe to continue with the next entry, so don't abort
                getReport().record(newError(ExportMigrationEntryImpl.FAILED_TO_BE_EXPORTED, e));
            } catch (MigrationException e) {
                throw e;
            }
        }
        return stored;
    }

    @Override
    public Optional<ExportMigrationEntry> getPropertyReferencedEntry(String name) {
        return AccessUtils.doPrivileged(() -> getPropertyReferencedEntry(name, (r, n) -> true));
    }

    @Override
    public Optional<ExportMigrationEntry> getPropertyReferencedEntry(String pname,
            BiPredicate<MigrationReport, String> validator) {
        Validate.notNull(pname, "invalid null java property name");
        Validate.notNull(validator, "invalid null validator");
        final ExportMigrationJavaPropertyReferencedEntryImpl me = properties.get(pname);

        if (me != null) {
            return Optional.of(me);
        }
        try {
            final String val = getJavaPropertyValue(pname);

            if (!validator.test(getReport(), val)) {
                return Optional.empty();
            } else if (val == null) {
                getReport().record(
                        new MigrationException(Messages.EXPORT_JAVA_PROPERTY_NOT_DEFINED_ERROR, pname, path));
                return Optional.empty();
            } else if (val.isEmpty()) {
                getReport()
                        .record(new MigrationException(Messages.EXPORT_JAVA_PROPERTY_IS_EMPTY_ERROR, pname, path));
                return Optional.empty();
            }
            final ExportMigrationJavaPropertyReferencedEntryImpl prop = new ExportMigrationJavaPropertyReferencedEntryImpl(
                    context, path, pname, val);

            properties.put(pname, prop);
            return Optional.of(prop);
        } catch (IOException e) {
            getReport().record(new MigrationException(Messages.EXPORT_JAVA_PROPERTY_LOAD_ERROR, pname, path, e));
            return Optional.empty();
        }
    }

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

    protected Path getAbsolutePath() {
        return absolutePath;
    }

    protected File getFile() {
        return file;
    }

    /** Called to record that this entry is being processed. */
    protected void recordEntry() { // nothing to record here
    }

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

    protected MigrationWarning newWarning(String reason) {
        return new MigrationWarning(Messages.EXPORT_PATH_WARNING, path, reason);
    }

    protected MigrationException newError(String reason, Object cause) {
        return new MigrationException(Messages.EXPORT_PATH_ERROR, path, reason, cause);
    }

    private boolean handleStoreWhenFilterMismatch(boolean required) {
        // we have a filter that doesn't match this entry and the entry represents
        // a file, so treat it as if the file was not there to start with
        if (required) {
            getReport().record(newError("was not copied", "it does not match filter"));
            return false;
        } // else - optional so no warnings/errors - just skip it and treat it as successful
        return true;
    }

    private boolean handleStoreWhenNoSuchFile(boolean required) {
        // we cannot rely on file.exists() here since the path is not valid anyway
        // relying on the exception is much safer and gives us the true story
        if (required) {
            getReport().record(newError("does not exist", absolutePathError));
            return false;
        } // else - optional so no warnings/errors - just skip it so treat it as successful
        return true;
    }

    private boolean handleStoreWhenFileExist(@Nullable PathMatcher filter) {
        return AccessUtils.doPrivileged(() -> {
            if (isMigratable()) {
                recordEntry();
                return isFile ? handleStoreFile() : handleStoreDirectory(filter);
            } // else - if it ain't migratable then only a warning occurs so return true
            return true;
        });
    }

    private boolean handleStoreFile() {
        try (final OutputStream os = getOutputStreamWithoutRecordingEntry()) {
            context.getReport().recordFile(this);
            FileUtils.copyFile(file, os);
            SecurityLogger.audit("Exported file {}", absolutePath);
            return true;
        } catch (ExportIOException e) {
            // special case indicating the I/O error occurred while writing to the zip which
            // would invalidate the zip so we are forced to abort
            SecurityLogger.audit(ExportMigrationEntryImpl.ERROR_EXPORTING_FILE, absolutePath);
            throw newError(ExportMigrationEntryImpl.FAILED_TO_BE_EXPORTED, e.getCause());
        } catch (IOException e) {
            // here it means the error came out of reading/processing the input file/stream
            // where it is safe to continue with the next entry, so don't abort
            SecurityLogger.audit(ExportMigrationEntryImpl.ERROR_EXPORTING_FILE, absolutePath);
            getReport().record(newError(ExportMigrationEntryImpl.FAILED_TO_BE_EXPORTED, e));
        }
        return false;
    }

    private boolean handleStoreDirectory(@Nullable PathMatcher filter) {
        final Set<String> files = new HashSet<>();
        // all files underneath the directory are optional so pass false to store()
        final boolean dirStored = ((filter != null) ? context.entries(path, filter) : context.entries(path))
                .peek(e -> files.add(e.getName())).map(e -> e.store(false)).reduce(true, Boolean::logicalAnd);

        if (dirStored) {
            context.getReport().recordDirectory(this, filter, files);
            SecurityLogger.audit("Exported directory {}", absolutePath);
        } else {
            SecurityLogger.audit("Error exporting directory {}", absolutePath);
            getReport().record(
                    newError(ExportMigrationEntryImpl.FAILED_TO_BE_EXPORTED, "some directory entries failed"));
        }
        return dirStored;
    }

    private OutputStream getOutputStreamWithoutRecordingEntry() throws IOException {
        try {
            return outputStream.updateAndGet(os -> (os != null) ? os : context.getOutputStreamFor(this));
        } catch (UncheckedIOException e) {
            throw e.getCause();
        }
    }

    private boolean isMigratable() {
        final ExportMigrationReportImpl report = context.getReport();

        if (path.isAbsolute()) {
            report.recordExternal(this, false);
            report.record(newWarning(String.format("is outside [%s]", context.getPathUtils().getDDFHome())));
            return false;
        } else if (Files.isSymbolicLink(absolutePath)) {
            report.recordExternal(this, true);
            report.record(newWarning("is a symbolic link"));
            return false;
        }
        return true;
    }

    @SuppressWarnings({ //
            "squid:S2093", /* try-with-resource will throw IOException with InputStream and we do not care to get that exception */
            "squid:S2095" /* stream is closed in finally clause */
    })
    private String getJavaPropertyValue(String pname) throws IOException {
        final Properties props = new Properties();
        InputStream is = null;

        try {
            is = new BufferedInputStream(new FileInputStream(file));
            props.load(is);
        } finally {
            IOUtils.closeQuietly(is);
        }
        return props.getProperty(pname);
    }

    /**
     * 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();
    }
}