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