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     */
019    package org.apache.commons.compress.changes;
020    
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.util.Enumeration;
024    import java.util.Iterator;
025    import java.util.LinkedHashSet;
026    import java.util.Set;
027    
028    import org.apache.commons.compress.archivers.ArchiveEntry;
029    import org.apache.commons.compress.archivers.ArchiveInputStream;
030    import org.apache.commons.compress.archivers.ArchiveOutputStream;
031    import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
032    import org.apache.commons.compress.archivers.zip.ZipFile;
033    import 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     */
044    public 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(ArchiveInputStream in, 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(ZipFile in, 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(ArchiveEntryIterator entryIterator,
112                                         ArchiveOutputStream out)
113                throws IOException {
114            ChangeSetResults results = new ChangeSetResults();
115    
116            Set<Change> workingSet = new LinkedHashSet<Change>(changes);
117    
118            for (Iterator<Change> it = workingSet.iterator(); it.hasNext();) {
119                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                ArchiveEntry entry = entryIterator.next();
130                boolean copy = true;
131    
132                for (Iterator<Change> it = workingSet.iterator(); it.hasNext();) {
133                    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
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 (Iterator<Change> it = workingSet.iterator(); it.hasNext();) {
164                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(Set<Change> workingSet, ArchiveEntry entry) {
188            String source = entry.getName();
189    
190            if (!workingSet.isEmpty()) {
191                for (Change change : workingSet) {
192                    final int type = change.type();
193                    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(InputStream in, ArchiveOutputStream out,
219                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(ArchiveInputStream in) {
245                this.in = in;
246            }
247            public boolean hasNext() throws IOException {
248                return (next = in.getNextEntry()) != null;
249            }
250            public ArchiveEntry next() {
251                return next;
252            }
253            public InputStream getInputStream() {
254                return in;
255            }
256        }
257    
258        private static class ZipFileIterator
259            implements ArchiveEntryIterator {
260            private final ZipFile in;
261            private final Enumeration<ZipArchiveEntry> nestedEnum;
262            private ZipArchiveEntry current;
263            ZipFileIterator(ZipFile in) {
264                this.in = in;
265                nestedEnum = in.getEntriesInPhysicalOrder();
266            }
267            public boolean hasNext() {
268                return nestedEnum.hasMoreElements();
269            }
270            public ArchiveEntry next() {
271                return (current = nestedEnum.nextElement());
272            }
273            public InputStream getInputStream() throws IOException {
274                return in.getInputStream(current);
275            }
276        }
277    }