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