All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.compress.changes.ChangeSetPerformer Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 1.27.1
Show newest version
/*
 * 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); } }