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