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