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