    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 * http://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;
    034import org.apache.commons.compress.utils.IOUtils;
    035
    036/**
    037 * 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
    038 * changes, a new Performer must be created.
    039 *
    040 * @param <I> The {@link ArchiveInputStream} type.
    041 * @param <O> The {@link ArchiveOutputStream} type.
    042 * @param <E> The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
    043 * @ThreadSafe
    044 * @Immutable
    045 */
    046public class ChangeSetPerformer<I extends ArchiveInputStream<E>, O extends ArchiveOutputStream<E>, E extends ArchiveEntry> {
    047
    048    /**
    049     * Abstracts getting entries and streams for archive entries.
    050     *
    051     * <p>
    052     * 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
    053     * exceptions thrown in ArchiveInputStream#getNextEntry.
    054     * </p>
    055     */
    056    private interface ArchiveEntryIterator<E extends ArchiveEntry> {
    057
    058        InputStream getInputStream() throws IOException;
    059
    060        boolean hasNext() throws IOException;
    061
    062        E next();
    063    }
    064
    065    private static final class ArchiveInputStreamIterator<E extends ArchiveEntry> implements ArchiveEntryIterator<E> {
    066
    067        private final ArchiveInputStream<E> inputStream;
    068        private E next;
    069
    070        ArchiveInputStreamIterator(final ArchiveInputStream<E> inputStream) {
    071            this.inputStream = inputStream;
    072        }
    073
    074        @Override
    075        public InputStream getInputStream() {
    076            return inputStream;
    077        }
    078
    079        @Override
    080        public boolean hasNext() throws IOException {
    081            return (next = inputStream.getNextEntry()) != null;
    082        }
    083
    084        @Override
    085        public E next() {
    086            return next;
    087        }
    088    }
    089
    090    private static final class ZipFileIterator implements ArchiveEntryIterator<ZipArchiveEntry> {
    091
    092        private final ZipFile zipFile;
    093        private final Enumeration<ZipArchiveEntry> nestedEnumeration;
    094        private ZipArchiveEntry currentEntry;
    095
    096        ZipFileIterator(final ZipFile zipFile) {
    097            this.zipFile = zipFile;
    098            this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
    099        }
    100
    101        @Override
    102        public InputStream getInputStream() throws IOException {
    103            return zipFile.getInputStream(currentEntry);
    104        }
    105
    106        @Override
    107        public boolean hasNext() {
    108            return nestedEnumeration.hasMoreElements();
    109        }
    110
    111        @Override
    112        public ZipArchiveEntry next() {
    113            return currentEntry = nestedEnumeration.nextElement();
    114        }
    115    }
    116
    117    private final Set<Change<E>> changes;
    118
    119    /**
    120     * Constructs a ChangeSetPerformer with the changes from this ChangeSet
    121     *
    122     * @param changeSet the ChangeSet which operations are used for performing
    123     */
    124    public ChangeSetPerformer(final ChangeSet<E> changeSet) {
    125        this.changes = changeSet.getChanges();
    126    }
    127
    128    /**
    129     * Copies the ArchiveEntry to the Output stream
    130     *
    131     * @param inputStream  the stream to read the data from
    132     * @param outputStream the stream to write the data to
    133     * @param archiveEntry the entry to write
    134     * @throws IOException if data cannot be read or written
    135     */
    136    private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
    137        outputStream.putArchiveEntry(archiveEntry);
    138        IOUtils.copy(inputStream, outputStream);
    139        outputStream.closeArchiveEntry();
    140    }
    141
    142    /**
    143     * 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
    144     * same set.
    145     *
    146     * @param entry the entry to check
    147     * @return true, if this entry has a deletion change later, false otherwise
    148     */
    149    private boolean isDeletedLater(final Set<Change<E>> workingSet, final E entry) {
    150        final String source = entry.getName();
    151
    152        if (!workingSet.isEmpty()) {
    153            for (final Change<E> change : workingSet) {
    154                final ChangeType type = change.getType();
    155                final String target = change.getTargetFileName();
    156                if (type == ChangeType.DELETE && source.equals(target)) {
    157                    return true;
    158                }
    159
    160                if (type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
    161                    return true;
    162                }
    163            }
    164        }
    165        return false;
    166    }
    167
    168    /**
    169     * Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
    170     *
    171     * This method finishes the stream, no other entries should be added after that.
    172     *
    173     * @param entryIterator the entries to perform the changes on
    174     * @param outputStream  the resulting OutputStream with all modifications
    175     * @throws IOException if a read/write error occurs
    176     * @return the results of this operation
    177     */
    178    private ChangeSetResults perform(final ArchiveEntryIterator<E> entryIterator, final O outputStream) throws IOException {
    179        final ChangeSetResults results = new ChangeSetResults();
    180
    181        final Set<Change<E>> workingSet = new LinkedHashSet<>(changes);
    182
    183        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    184            final Change<E> change = it.next();
    185
    186            if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
    187                @SuppressWarnings("resource") // InputStream not allocated here
    188                final InputStream inputStream = change.getInputStream();
    189                copyStream(inputStream, outputStream, change.getEntry());
    190                it.remove();
    191                results.addedFromChangeSet(change.getEntry().getName());
    192            }
    193        }
    194
    195        while (entryIterator.hasNext()) {
    196            final E entry = entryIterator.next();
    197            boolean copy = true;
    198
    199            for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    200                final Change<E> change = it.next();
    201
    202                final ChangeType type = change.getType();
    203                final String name = entry.getName();
    204                if (type == ChangeType.DELETE && name != null) {
    205                    if (name.equals(change.getTargetFileName())) {
    206                        copy = false;
    207                        it.remove();
    208                        results.deleted(name);
    209                        break;
    210                    }
    211                } else if (type == ChangeType.DELETE_DIR && name != null) {
    212                    // don't combine ifs to make future extensions more easy
    213                    if (name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
    214                        copy = false;
    215                        results.deleted(name);
    216                        break;
    217                    }
    218                }
    219            }
    220
    221            if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
    222                @SuppressWarnings("resource") // InputStream not allocated here
    223                final InputStream inputStream = entryIterator.getInputStream();
    224                copyStream(inputStream, outputStream, entry);
    225                results.addedFromStream(entry.getName());
    226            }
    227        }
    228
    229        // Adds files which hasn't been added from the original and do not have replace mode on
    230        for (final Iterator<Change<E>> it = workingSet.iterator(); it.hasNext();) {
    231            final Change<E> change = it.next();
    232
    233            if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
    234                @SuppressWarnings("resource")
    235                final InputStream input = change.getInputStream();
    236                copyStream(input, outputStream, change.getEntry());
    237                it.remove();
    238                results.addedFromChangeSet(change.getEntry().getName());
    239            }
    240        }
    241        outputStream.finish();
    242        return results;
    243    }
    244
    245    /**
    246     * 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.
    247     *
    248     * This method finishes the stream, no other entries should be added after that.
    249     *
    250     * @param inputStream  the InputStream to perform the changes on
    251     * @param outputStream the resulting OutputStream with all modifications
    252     * @throws IOException if a read/write error occurs
    253     * @return the results of this operation
    254     */
    255    public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
    256        return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
    257    }
    258
    259    /**
    260     * 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.
    261     *
    262     * This method finishes the stream, no other entries should be added after that.
    263     *
    264     * @param zipFile      the ZipFile to perform the changes on
    265     * @param outputStream the resulting OutputStream with all modifications
    266     * @throws IOException if a read/write error occurs
    267     * @return the results of this operation
    268     * @since 1.5
    269     */
    270    public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
    271        @SuppressWarnings("unchecked")
    272        final ArchiveEntryIterator<E> entryIterator = (ArchiveEntryIterator<E>) new ZipFileIterator(zipFile);
    273        return perform(entryIterator, outputStream);
    274    }
    275}