    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 *   https://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.InputStream;
    022import java.util.Iterator;
    023import java.util.LinkedHashSet;
    024import java.util.Set;
    025import java.util.regex.Pattern;
    026
    027import org.apache.commons.compress.archivers.ArchiveEntry;
    028import org.apache.commons.compress.changes.Change.ChangeType;
    029
    030/**
    031 * ChangeSet collects and performs changes to an archive. Putting delete changes in this ChangeSet from multiple threads can cause conflicts.
    032 *
    033 * @param <E> The ArchiveEntry type.
    034 * @NotThreadSafe
    035 */
    036public final class ChangeSet<E extends ArchiveEntry> {
    037
    038    private final Set<Change<E>> changes = new LinkedHashSet<>();
    039
    040    /**
    041     * Constructs a new instance.
    042     */
    043    public ChangeSet() {
    044        // empty
    045    }
    046
    047    /**
    048     * Adds a new archive entry to the archive.
    049     *
    050     * @param entry the entry to add
    051     * @param input the data stream to add
    052     */
    053    public void add(final E entry, final InputStream input) {
    054        this.add(entry, input, true);
    055    }
    056
    057    /**
    058     * Adds a new archive entry to the archive. If replace is set to true, this change will replace all other additions done in this ChangeSet and all existing
    059     * entries in the original stream.
    060     *
    061     * @param entry   the entry to add
    062     * @param input   the data stream to add
    063     * @param replace indicates the this change should replace existing entries
    064     */
    065    public void add(final E entry, final InputStream input, final boolean replace) {
    066        addAddition(new Change<>(entry, input, replace));
    067    }
    068
    069    /**
    070     * Adds an addition change.
    071     *
    072     * @param addChange the change which should result in an addition
    073     */
    074    @SuppressWarnings("resource") // InputStream is NOT allocated
    075    private void addAddition(final Change<E> addChange) {
    076        if (Change.ChangeType.ADD != addChange.getType() || addChange.getInputStream() == null) {
    077            return;
    078        }
    079
    080        if (!changes.isEmpty()) {
    081            for (final Iterator<Change<E>> it = changes.iterator(); it.hasNext();) {
    082                final Change<E> change = it.next();
    083                if (change.getType() == Change.ChangeType.ADD && change.getEntry() != null) {
    084                    final ArchiveEntry entry = change.getEntry();
    085
    086                    if (entry.equals(addChange.getEntry())) {
    087                        if (addChange.isReplaceMode()) {
    088                            it.remove();
    089                            changes.add(addChange);
    090                        }
    091                        // do not add this change
    092                        return;
    093                    }
    094                }
    095            }
    096        }
    097        changes.add(addChange);
    098    }
    099
    100    /**
    101     * Adds an delete change.
    102     *
    103     * @param deleteChange the change which should result in a deletion
    104     */
    105    private void addDeletion(final Change<E> deleteChange) {
    106        if (ChangeType.DELETE != deleteChange.getType() && ChangeType.DELETE_DIR != deleteChange.getType() || deleteChange.getTargetFileName() == null) {
    107            return;
    108        }
    109        final String source = deleteChange.getTargetFileName();
    110        final Pattern pattern = Pattern.compile(source + "/.*");
    111        if (source != null && !changes.isEmpty()) {
    112            for (final Iterator<Change<E>> it = changes.iterator(); it.hasNext();) {
    113                final Change<E> change = it.next();
    114                if (change.getType() == ChangeType.ADD && change.getEntry() != null) {
    115                    final String target = change.getEntry().getName();
    116                    if (target == null) {
    117                        continue;
    118                    }
    119                    if (ChangeType.DELETE == deleteChange.getType() && source.equals(target)
    120                            || ChangeType.DELETE_DIR == deleteChange.getType() && pattern.matcher(target).matches()) {
    121                        it.remove();
    122                    }
    123                }
    124            }
    125        }
    126        changes.add(deleteChange);
    127    }
    128
    129    /**
    130     * Deletes the file with the file name from the archive.
    131     *
    132     * @param fileName the file name of the file to delete
    133     */
    134    public void delete(final String fileName) {
    135        addDeletion(new Change<>(fileName, ChangeType.DELETE));
    136    }
    137
    138    /**
    139     * Deletes the directory tree from the archive.
    140     *
    141     * @param dirName the name of the directory tree to delete
    142     */
    143    public void deleteDir(final String dirName) {
    144        addDeletion(new Change<>(dirName, ChangeType.DELETE_DIR));
    145    }
    146
    147    /**
    148     * Gets the list of changes as a copy. Changes on this set are not reflected on this ChangeSet and vice versa.
    149     *
    150     * @return the changes as a copy
    151     */
    152    Set<Change<E>> getChanges() {
    153        return new LinkedHashSet<>(changes);
    154    }
    155}