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}