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}