    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 *   https://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.changes;
    020
    021import java.io.IOException;
    022import java.io.InputStream;
    023import java.util.Enumeration;
    024import java.util.Iterator;
    025import java.util.LinkedHashSet;
    026import java.util.Set;
    027
    028import org.apache.commons.compress.archivers.ArchiveEntry;
    029import org.apache.commons.compress.archivers.ArchiveInputStream;
    030import org.apache.commons.compress.archivers.ArchiveOutputStream;
    031import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
    032import org.apache.commons.compress.archivers.zip.ZipFile;
    033import org.apache.commons.compress.changes.Change.ChangeType;
    034
    035/**
    036 * Performs ChangeSet operations on a stream. This class is thread safe and can be used multiple times. It operates on a copy of the ChangeSet. If the ChangeSet
    037 * changes, a new Performer must be created.
    038 *
    039 * @param <I> The {@link ArchiveInputStream} type.
    040 * @param <O> The {@link ArchiveOutputStream} type.
    041 * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
    042 * @ThreadSafe
    043 * @Immutable
    044 */
    045public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {
    046
    047    /**
    048     * Abstracts getting entries and streams for archive entries.
    049     *
    050     * <p>
    051     * Iterator#hasNext is not allowed to throw exceptions that's why we can't use Iterator&lt;ArchiveEntry&gt; directly - otherwise we'd need to convert
    052     * exceptions thrown in ArchiveInputStream#getNextEntry.
    053     * </p>
    054     */
    055    private interface ArchiveEntryIterator<E extends ArchiveEntry> {
    056
    057        InputStream getInputStream() throws IOException;
    058
    059        boolean hasNext() throws IOException;
    060
    061        E next();
    062    }
    063
    064    private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {
    065
    066        private final ArchiveInputStream<E> inputStream;
    067        private E next;
    068
    069        ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
    070            this.inputStream = inputStream;
    071        }
    072
    073        @Override
    074        public InputStream getInputStream() {
    075            return inputStream;
    076        }
    077
    078        @Override
    079        public boolean hasNext() throws IOException {
    080            return (next = inputStream.getNextEntry()) != null;
    081        }
    082
    083        @Override
    084        public E next() {
    085            return next;
    086        }
    087    }
    088
    089    private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {
    090
    091        private final ZipFile zipFile;
    092        private final Enumeration<ZipArchiveEntry> nestedEnumeration;
    093        private ZipArchiveEntry currentEntry;
    094
    095        ZipFileIterator(final ZipFile zipFile) {
    096            this.zipFile = zipFile;
    097            this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
    098        }
    099
    100        @Override
    101        public InputStream getInputStream() throws IOException {
    102            return zipFile.getInputStream(currentEntry);
    103        }
    104
    105        @Override
    106        public boolean hasNext() {
    107            return nestedEnumeration.hasMoreElements();
    108        }
    109
    110        @Override
    111        public ZipArchiveEntry next() {
    112            return currentEntry = nestedEnumeration.nextElement();
    113        }
    114    }
    115
    116    private final Set<Change<E>> changes;
    117
    118    /**
    119     * Constructs a ChangeSetPerformer with the changes from this ChangeSet
    120     *
    121     * @param changeSet the ChangeSet which operations are used for performing
    122     */
    123    public ChangeSetPerformer(final ChangeSet<E> changeSet) {
    124        this.changes = changeSet.getChanges();
    125    }
    126
    127    /**
    128     * Copies the ArchiveEntry to the Output stream
    129     *
    130     * @param inputStream  the stream to read the data from
    131     * @param outputStream the stream to write the data to
    132     * @param archiveEntry the entry to write
    133     * @throws IOException if data cannot be read or written
    134     */
    135    private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
    136        outputStream.putArchiveEntry(archiveEntry);
    137        org.apache.commons.io.IOUtils.copy(inputStream, outputStream);
    138        outputStream.closeArchiveEntry();
    139    }
    140
    141    /**
    142     * Checks if an ArchiveEntry is deleted later in the ChangeSet. This is necessary if a file is added with this ChangeSet, but later became deleted in the
    143     * same set.
    144     *
    145     * @param entry the entry to check
    146     * @return true, if this entry has a deletion change later, false otherwise
    147     */
    148    private boolean isDeletedLater(final Set<Change<E>> workingSet, final E entry) {
    149        final String source = entry.getName();
    150
    151        if (!workingSet.isEmpty()) {
    152            for (final Change<E> change : workingSet) {
    153                final ChangeType type = change.getType();
    154                final String target = change.getTargetFileName();
    155                if (type == ChangeType.DELETE && source.equals(target) || type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
    156                    return true;
    157                }
    158            }
    159        }
    160        return false;
    161    }
    162
    163    /**
    164     * Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
    165     *
    166     * This method finishes the stream, no other entries should be added after that.
    167     *
    168     * @param entryIterator the entries to perform the changes on
    169     * @param outputStream  the resulting OutputStream with all modifications
    170     * @throws IOException if a read/write error occurs
    171     * @return the results of this operation
    172     */
    173    private ChangeSetResults perform(final ArchiveEntryIterator<E> entryIterator, final O outputStream) throws IOException {
    174        final ChangeSetResults results = new ChangeSetResults();
    175
    176        final Set<Change<E>> workingSet = new LinkedHashSet<>(changes);
    177
    178        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    179            final Change<E> change = it.next();
    180
    181            if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
    182                @SuppressWarnings("resource") // InputStream not allocated here
    183                final InputStream inputStream = change.getInputStream();
    184                copyStream(inputStream, outputStream, change.getEntry());
    185                it.remove();
    186                results.addedFromChangeSet(change.getEntry().getName());
    187            }
    188        }
    189
    190        while (entryIterator.hasNext()) {
    191            final E entry = entryIterator.next();
    192            boolean copy = true;
    193
    194            for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    195                final Change<E> change = it.next();
    196
    197                final ChangeType type = change.getType();
    198                final String name = entry.getName();
    199                if (type == ChangeType.DELETE && name != null) {
    200                    if (name.equals(change.getTargetFileName())) {
    201                        copy = false;
    202                        it.remove();
    203                        results.deleted(name);
    204                        break;
    205                    }
    206                } else // don't combine ifs to make future extensions more easy
    207                if (type == ChangeType.DELETE_DIR && name != null && name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
    208                    copy = false;
    209                    results.deleted(name);
    210                    break;
    211                }
    212            }
    213
    214            if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
    215                @SuppressWarnings("resource") // InputStream not allocated here
    216                final InputStream inputStream = entryIterator.getInputStream();
    217                copyStream(inputStream, outputStream, entry);
    218                results.addedFromStream(entry.getName());
    219            }
    220        }
    221
    222        // Adds files which hasn't been added from the original and do not have replace mode on
    223        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    224            final Change<E> change = it.next();
    225
    226            if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
    227                @SuppressWarnings("resource")
    228                final InputStream input = change.getInputStream();
    229                copyStream(input, outputStream, change.getEntry());
    230                it.remove();
    231                results.addedFromChangeSet(change.getEntry().getName());
    232            }
    233        }
    234        outputStream.finish();
    235        return results;
    236    }
    237
    238    /**
    239     * Performs all changes collected in this ChangeSet on the input stream and streams the result to the output stream. Perform may be called more than once.
    240     *
    241     * This method finishes the stream, no other entries should be added after that.
    242     *
    243     * @param inputStream  the InputStream to perform the changes on
    244     * @param outputStream the resulting OutputStream with all modifications
    245     * @throws IOException if a read/write error occurs
    246     * @return the results of this operation
    247     */
    248    public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
    249        return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
    250    }
    251
    252    /**
    253     * Performs all changes collected in this ChangeSet on the ZipFile and streams the result to the output stream. Perform may be called more than once.
    254     *
    255     * This method finishes the stream, no other entries should be added after that.
    256     *
    257     * @param zipFile      the ZipFile to perform the changes on
    258     * @param outputStream the resulting OutputStream with all modifications
    259     * @throws IOException if a read/write error occurs
    260     * @return the results of this operation
    261     * @since 1.5
    262     */
    263    public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
    264        @SuppressWarnings("unchecked")
    265        final ArchiveEntryIterator<E> entryIterator = (ArchiveEntryIterator<E>) new ZipFileIterator(zipFile);
    266        return perform(entryIterator, outputStream);
    267    }
    268}