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<ArchiveEntry> 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}