All Downloads are FREE. Search and download functionalities are using the official Maven repository.

global.namespace.fun.io.delta.ArchivePatch Maven / Gradle / Ivy

There is a newer version: 2.4.1
Show newest version
/*
 * Copyright (C) 2013-2018 Schlichtherle IT Services.
 * All rights reserved. Use is subject to license terms.
 */
package global.namespace.fun.io.delta;

import global.namespace.fun.io.api.*;
import global.namespace.fun.io.api.function.XConsumer;
import global.namespace.fun.io.delta.model.DeltaModel;
import global.namespace.fun.io.delta.model.EntryNameAndDigestValue;

import java.io.IOException;
import java.security.MessageDigest;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static global.namespace.fun.io.delta.Delta.decodeModel;
import static java.util.Arrays.asList;

/**
 * Patches a base archive file to an update archive file using a delta archive file.
 *
 * @author Christian Schlichtherle
 */
abstract class ArchivePatch {

    abstract ArchiveSource baseSource();

    abstract ArchiveSource deltaSource();

     void to(ArchiveSink update) throws Exception {
        this.accept(engine -> update.acceptWriter(engine::to));
    }

    private  void accept(XConsumer> consumer) throws Exception {
        baseSource().acceptReader(baseInput -> deltaSource().acceptReader(deltaInput -> consumer.accept(
                new Engine() {

                    ArchiveInput baseInput() { return baseInput; }

                    ArchiveInput deltaInput() { return deltaInput; }
                }
        )));
    }

    private abstract class Engine {

        DeltaModel model;

        WithMessageDigest digest;

        abstract ArchiveInput baseInput();

        abstract ArchiveInput deltaInput();

        void to(final ArchiveOutput updateOutput) throws Exception {
            for (Predicate filter : passFilters(updateOutput)) {
                to(updateOutput, filter);
            }
        }

        /**
         * Returns a list of filters for the different passes required to generate the update archive file.
         * At least one filter is required to output anything.
         * The filters should properly partition the set of entry sources, i.e. each entry source should be accepted by
         * exactly one filter.
         */
        Iterable> passFilters(final ArchiveOutput updateOutput) {
            if (updateOutput.isJar()) {
                // java.util.JarInputStream assumes that the file entry
                // "META-INF/MANIFEST.MF" should either be the first or the second
                // entry (if preceded by the directory entry "META-INF/"), so we
                // need to process the delta-archive file in two passes with a
                // corresponding filter to ensure this order.
                // Note that the directory entry "META-INF/" is always part of the
                // unchanged delta set because it's content is always empty.
                // Thus, by copying the unchanged entries before the changed
                // entries, the directory entry "META-INF/" will always appear
                // before the file entry "META-INF/MANIFEST.MF".
                final Predicate manifestFilter = "META-INF/MANIFEST.MF"::equals;
                return asList(manifestFilter, manifestFilter.negate());
            } else {
                return Collections.singletonList(t -> true);
            }
        }

        void to(final ArchiveOutput updateOutput, final Predicate filter) throws Exception {

            abstract class Patch {

                abstract ArchiveInput input();

                abstract IOException ioException(Throwable cause);

                final void apply(final Collection collection) throws Exception {
                    for (final EntryNameAndDigestValue entryNameAndDigestValue : collection) {
                        final String name = entryNameAndDigestValue.name();
                        if (filter.test(name)) {
                            final Optional> optEntry = input().source(name);
                            if (optEntry.isPresent()) {
                                final ArchiveEntrySource entry = optEntry.get();
                                if (!digestValueOf(entry).equals(entryNameAndDigestValue.digestValue())) {
                                    throw ioException(new WrongMessageDigestException(name));
                                }
                                entry.copyTo(updateOutput.sink(name));
                            } else {
                                throw ioException(new MissingArchiveEntryException(name));
                            }
                        }
                    }
                }
            }

            class OnBaseInputPatch extends Patch {

                @Override
                ArchiveInput input() { return baseInput(); }

                @Override
                IOException ioException(Throwable cause) { return new WrongBaseArchiveFileException(cause); }
            }

            class OnDeltaInputPatch extends Patch {

                @Override
                ArchiveInput input() { return deltaInput(); }

                @Override
                IOException ioException(Throwable cause) { return new InvalidDeltaArchiveFileException(cause); }
            }

            // Order is important here!
            new OnBaseInputPatch().apply(model().unchangedEntries());
            new OnDeltaInputPatch().apply(model().changedEntries()
                    .stream()
                    .map(change -> new EntryNameAndDigestValue(change.name(), change.updateDigestValue()))
                    .collect(Collectors.toList()));
            new OnDeltaInputPatch().apply(model().addedEntries());
        }

        String digestValueOf(Source source) throws Exception { return digest().digestValueOf(source); }

        WithMessageDigest digest() throws Exception {
            final WithMessageDigest digest = this.digest;
            return null != digest
                    ? digest
                    : (this.digest = WithMessageDigest.of(MessageDigest.getInstance(model().digestAlgorithmName())));
        }

        DeltaModel model() throws Exception {
            final DeltaModel model = this.model;
            return null != model ? model : (this.model = decodeModel(deltaInput()));
        }
    }
}