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