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