org.apache.commons.compress.changes.ChangeSetPerformer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of commons-compress Show documentation
Show all versions of commons-compress Show documentation
Apache Commons Compress software defines an API for working with
compression and archive formats. These include: bzip2, gzip, pack200,
lzma, xz, Snappy, traditional Unix Compress, DEFLATE, DEFLATE64, LZ4,
Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.changes;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.changes.Change.ChangeType;
/**
* 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
* changes, a new Performer must be created.
*
* @param The {@link ArchiveInputStream} type.
* @param The {@link ArchiveOutputStream} type.
* @param The {@link ArchiveEntry} type, must be compatible between the input {@code I} and output {@code O} stream types.
* @ThreadSafe
* @Immutable
*/
public class ChangeSetPerformer, O extends ArchiveOutputStream, E extends ArchiveEntry> {
/**
* Abstracts getting entries and streams for archive entries.
*
*
* Iterator#hasNext is not allowed to throw exceptions that's why we can't use Iterator<ArchiveEntry> directly - otherwise we'd need to convert
* exceptions thrown in ArchiveInputStream#getNextEntry.
*
*/
private interface ArchiveEntryIterator {
InputStream getInputStream() throws IOException;
boolean hasNext() throws IOException;
E next();
}
private static final class ArchiveInputStreamIterator implements ArchiveEntryIterator {
private final ArchiveInputStream inputStream;
private E next;
ArchiveInputStreamIterator(final ArchiveInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public InputStream getInputStream() {
return inputStream;
}
@Override
public boolean hasNext() throws IOException {
return (next = inputStream.getNextEntry()) != null;
}
@Override
public E next() {
return next;
}
}
private static final class ZipFileIterator implements ArchiveEntryIterator {
private final ZipFile zipFile;
private final Enumeration nestedEnumeration;
private ZipArchiveEntry currentEntry;
ZipFileIterator(final ZipFile zipFile) {
this.zipFile = zipFile;
this.nestedEnumeration = zipFile.getEntriesInPhysicalOrder();
}
@Override
public InputStream getInputStream() throws IOException {
return zipFile.getInputStream(currentEntry);
}
@Override
public boolean hasNext() {
return nestedEnumeration.hasMoreElements();
}
@Override
public ZipArchiveEntry next() {
return currentEntry = nestedEnumeration.nextElement();
}
}
private final Set> changes;
/**
* Constructs a ChangeSetPerformer with the changes from this ChangeSet
*
* @param changeSet the ChangeSet which operations are used for performing
*/
public ChangeSetPerformer(final ChangeSet changeSet) {
this.changes = changeSet.getChanges();
}
/**
* Copies the ArchiveEntry to the Output stream
*
* @param inputStream the stream to read the data from
* @param outputStream the stream to write the data to
* @param archiveEntry the entry to write
* @throws IOException if data cannot be read or written
*/
private void copyStream(final InputStream inputStream, final O outputStream, final E archiveEntry) throws IOException {
outputStream.putArchiveEntry(archiveEntry);
org.apache.commons.io.IOUtils.copy(inputStream, outputStream);
outputStream.closeArchiveEntry();
}
/**
* 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
* same set.
*
* @param entry the entry to check
* @return true, if this entry has a deletion change later, false otherwise
*/
private boolean isDeletedLater(final Set> workingSet, final E entry) {
final String source = entry.getName();
if (!workingSet.isEmpty()) {
for (final Change change : workingSet) {
final ChangeType type = change.getType();
final String target = change.getTargetFileName();
if (type == ChangeType.DELETE && source.equals(target)) {
return true;
}
if (type == ChangeType.DELETE_DIR && source.startsWith(target + "/")) {
return true;
}
}
}
return false;
}
/**
* Performs all changes collected in this ChangeSet on the input entries and streams the result to the output stream.
*
* This method finishes the stream, no other entries should be added after that.
*
* @param entryIterator the entries to perform the changes on
* @param outputStream the resulting OutputStream with all modifications
* @throws IOException if a read/write error occurs
* @return the results of this operation
*/
private ChangeSetResults perform(final ArchiveEntryIterator entryIterator, final O outputStream) throws IOException {
final ChangeSetResults results = new ChangeSetResults();
final Set> workingSet = new LinkedHashSet<>(changes);
for (final Iterator> it = workingSet.iterator(); it.hasNext();) {
final Change change = it.next();
if (change.getType() == ChangeType.ADD && change.isReplaceMode()) {
@SuppressWarnings("resource") // InputStream not allocated here
final InputStream inputStream = change.getInputStream();
copyStream(inputStream, outputStream, change.getEntry());
it.remove();
results.addedFromChangeSet(change.getEntry().getName());
}
}
while (entryIterator.hasNext()) {
final E entry = entryIterator.next();
boolean copy = true;
for (final Iterator> it = workingSet.iterator(); it.hasNext();) {
final Change change = it.next();
final ChangeType type = change.getType();
final String name = entry.getName();
if (type == ChangeType.DELETE && name != null) {
if (name.equals(change.getTargetFileName())) {
copy = false;
it.remove();
results.deleted(name);
break;
}
} else if (type == ChangeType.DELETE_DIR && name != null) {
// don't combine ifs to make future extensions more easy
if (name.startsWith(change.getTargetFileName() + "/")) { // NOPMD NOSONAR
copy = false;
results.deleted(name);
break;
}
}
}
if (copy && !isDeletedLater(workingSet, entry) && !results.hasBeenAdded(entry.getName())) {
@SuppressWarnings("resource") // InputStream not allocated here
final InputStream inputStream = entryIterator.getInputStream();
copyStream(inputStream, outputStream, entry);
results.addedFromStream(entry.getName());
}
}
// Adds files which hasn't been added from the original and do not have replace mode on
for (final Iterator> it = workingSet.iterator(); it.hasNext();) {
final Change change = it.next();
if (change.getType() == ChangeType.ADD && !change.isReplaceMode() && !results.hasBeenAdded(change.getEntry().getName())) {
@SuppressWarnings("resource")
final InputStream input = change.getInputStream();
copyStream(input, outputStream, change.getEntry());
it.remove();
results.addedFromChangeSet(change.getEntry().getName());
}
}
outputStream.finish();
return results;
}
/**
* 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.
*
* This method finishes the stream, no other entries should be added after that.
*
* @param inputStream the InputStream to perform the changes on
* @param outputStream the resulting OutputStream with all modifications
* @throws IOException if a read/write error occurs
* @return the results of this operation
*/
public ChangeSetResults perform(final I inputStream, final O outputStream) throws IOException {
return perform(new ArchiveInputStreamIterator<>(inputStream), outputStream);
}
/**
* 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.
*
* This method finishes the stream, no other entries should be added after that.
*
* @param zipFile the ZipFile to perform the changes on
* @param outputStream the resulting OutputStream with all modifications
* @throws IOException if a read/write error occurs
* @return the results of this operation
* @since 1.5
*/
public ChangeSetResults perform(final ZipFile zipFile, final O outputStream) throws IOException {
@SuppressWarnings("unchecked")
final ArchiveEntryIterator entryIterator = (ArchiveEntryIterator) new ZipFileIterator(zipFile);
return perform(entryIterator, outputStream);
}
}