1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.commons.compress.changes;
20
21 import java.io.InputStream;
22 import java.util.Iterator;
23 import java.util.LinkedHashSet;
24 import java.util.Set;
25 import java.util.regex.Pattern;
26
27 import org.apache.commons.compress.archivers.ArchiveEntry;
28 import org.apache.commons.compress.changes.Change.ChangeType;
29
30 /**
31 * ChangeSet collects and performs changes to an archive. Putting delete changes in this ChangeSet from multiple threads can cause conflicts.
32 *
33 * @param <E> The ArchiveEntry type.
34 * @NotThreadSafe
35 */
36 public final class ChangeSet<E extends ArchiveEntry> {
37
38 private final Set<Change<E>> changes = new LinkedHashSet<>();
39
40 /**
41 * Constructs a new instance.
42 */
43 public ChangeSet() {
44 // empty
45 }
46
47 /**
48 * Adds a new archive entry to the archive.
49 *
50 * @param entry the entry to add
51 * @param input the data stream to add
52 */
53 public void add(final E entry, final InputStream input) {
54 this.add(entry, input, true);
55 }
56
57 /**
58 * 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
59 * entries in the original stream.
60 *
61 * @param entry the entry to add
62 * @param input the data stream to add
63 * @param replace indicates the this change should replace existing entries
64 */
65 public void add(final E entry, final InputStream input, final boolean replace) {
66 addAddition(new Change<>(entry, input, replace));
67 }
68
69 /**
70 * Adds an addition change.
71 *
72 * @param addChange the change which should result in an addition
73 */
74 @SuppressWarnings("resource") // InputStream is NOT allocated
75 private void addAddition(final Change<E> addChange) {
76 if (Change.ChangeType.ADD != addChange.getType() || addChange.getInputStream() == null) {
77 return;
78 }
79
80 if (!changes.isEmpty()) {
81 for (final Iterator<Change<E>> it = changes.iterator(); it.hasNext();) {
82 final Change<E> change = it.next();
83 if (change.getType() == Change.ChangeType.ADD && change.getEntry() != null) {
84 final ArchiveEntry entry = change.getEntry();
85
86 if (entry.equals(addChange.getEntry())) {
87 if (addChange.isReplaceMode()) {
88 it.remove();
89 changes.add(addChange);
90 }
91 // do not add this change
92 return;
93 }
94 }
95 }
96 }
97 changes.add(addChange);
98 }
99
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 }