Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.account;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static java.util.Objects.requireNonNull;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.common.annotations.VisibleForTesting;
22 : import com.google.common.base.Enums;
23 : import com.google.common.base.Joiner;
24 : import com.google.common.base.Splitter;
25 : import com.google.common.base.Strings;
26 : import com.google.common.collect.ImmutableMap;
27 : import com.google.common.collect.ImmutableSet;
28 : import com.google.common.collect.ListMultimap;
29 : import com.google.common.collect.Multimap;
30 : import com.google.common.collect.MultimapBuilder;
31 : import com.google.common.collect.Sets;
32 : import com.google.gerrit.common.Nullable;
33 : import com.google.gerrit.entities.Account;
34 : import com.google.gerrit.entities.NotifyConfig;
35 : import com.google.gerrit.entities.Project;
36 : import com.google.gerrit.server.git.ValidationError;
37 : import java.util.ArrayList;
38 : import java.util.Collection;
39 : import java.util.EnumSet;
40 : import java.util.HashMap;
41 : import java.util.List;
42 : import java.util.Map;
43 : import java.util.Set;
44 : import org.eclipse.jgit.lib.Config;
45 :
46 : /**
47 : * Parses/writes project watches from/to a {@link Config} file.
48 : *
49 : * <p>This is a low-level API. Read/write of project watches in a user branch should be done through
50 : * {@link AccountsUpdate} or {@link AccountConfig}.
51 : *
52 : * <p>The config file has one 'project' section for all project watches of a project.
53 : *
54 : * <p>The project name is used as subsection name and the filters with the notify types that decide
55 : * for which events email notifications should be sent are represented as 'notify' values in the
56 : * subsection. A 'notify' value is formatted as {@code <filter>
57 : * [<comma-separated-list-of-notify-types>]}:
58 : *
59 : * <pre>
60 : * [project "foo"]
61 : * notify = * [ALL_COMMENTS]
62 : * notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
63 : * notify = branch:master owner:self [SUBMITTED_CHANGES]
64 : * </pre>
65 : *
66 : * <p>If two notify values in the same subsection have the same filter they are merged on the next
67 : * save, taking the union of the notify types.
68 : *
69 : * <p>For watch configurations that notify on no event the list of notify types is empty:
70 : *
71 : * <pre>
72 : * [project "foo"]
73 : * notify = branch:master []
74 : * </pre>
75 : *
76 : * <p>Unknown notify types are ignored and removed on save.
77 : *
78 : * <p>The project watches are lazily parsed.
79 : */
80 : public class ProjectWatches {
81 : @AutoValue
82 18 : public abstract static class ProjectWatchKey {
83 :
84 : public static ProjectWatchKey create(Project.NameKey project, @Nullable String filter) {
85 18 : return new AutoValue_ProjectWatches_ProjectWatchKey(project, Strings.emptyToNull(filter));
86 : }
87 :
88 : public abstract Project.NameKey project();
89 :
90 : public abstract @Nullable String filter();
91 : }
92 :
93 : public static final String FILTER_ALL = "*";
94 :
95 : public static final String WATCH_CONFIG = "watch.config";
96 : public static final String PROJECT = "project";
97 : public static final String KEY_NOTIFY = "notify";
98 :
99 : private final Account.Id accountId;
100 : private final Config cfg;
101 : private final ValidationError.Sink validationErrorSink;
102 :
103 : private ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> projectWatches;
104 :
105 151 : ProjectWatches(Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
106 151 : this.accountId = requireNonNull(accountId, "accountId");
107 151 : this.cfg = requireNonNull(cfg, "cfg");
108 151 : this.validationErrorSink = requireNonNull(validationErrorSink, "validationErrorSink");
109 151 : }
110 :
111 : public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> getProjectWatches() {
112 151 : if (projectWatches == null) {
113 151 : parse();
114 : }
115 151 : return projectWatches;
116 : }
117 :
118 : public void parse() {
119 151 : projectWatches = parse(accountId, cfg, validationErrorSink);
120 151 : }
121 :
122 : /**
123 : * Parses project watches from the given config file and returns them as a map.
124 : *
125 : * <p>A project watch is defined on a project and has a filter to match changes for which the
126 : * project watch should be applied. The project and the filter form the map key. The map value is
127 : * a set of notify types that decide for which events email notifications should be sent.
128 : *
129 : * <p>A project watch on the {@code All-Projects} project applies for all projects unless the
130 : * project has a matching project watch.
131 : *
132 : * <p>A project watch can have an empty set of notify types. An empty set of notify types means
133 : * that no notification for matching changes should be set. This is different from no project
134 : * watch as it overwrites matching project watches from the {@code All-Projects} project.
135 : *
136 : * <p>Since we must be able to differentiate a project watch with an empty set of notify types
137 : * from no project watch we can't use a {@link Multimap} as return type.
138 : *
139 : * @param accountId the ID of the account for which the project watches should be parsed
140 : * @param cfg the config file from which the project watches should be parsed
141 : * @param validationErrorSink validation error sink
142 : * @return the parsed project watches
143 : */
144 : @VisibleForTesting
145 : public static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> parse(
146 : Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
147 151 : Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches = new HashMap<>();
148 151 : for (String projectName : cfg.getSubsections(PROJECT)) {
149 18 : String[] notifyValues = cfg.getStringList(PROJECT, projectName, KEY_NOTIFY);
150 18 : for (String nv : notifyValues) {
151 18 : if (Strings.isNullOrEmpty(nv)) {
152 0 : continue;
153 : }
154 :
155 18 : NotifyValue notifyValue =
156 18 : NotifyValue.parse(accountId, projectName, nv, validationErrorSink);
157 18 : if (notifyValue == null) {
158 1 : continue;
159 : }
160 :
161 18 : ProjectWatchKey key =
162 18 : ProjectWatchKey.create(Project.nameKey(projectName), notifyValue.filter());
163 18 : if (!projectWatches.containsKey(key)) {
164 18 : projectWatches.put(key, EnumSet.noneOf(NotifyConfig.NotifyType.class));
165 : }
166 18 : projectWatches.get(key).addAll(notifyValue.notifyTypes());
167 : }
168 18 : }
169 151 : return immutableCopyOf(projectWatches);
170 : }
171 :
172 : public Config save(Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches) {
173 16 : this.projectWatches = immutableCopyOf(projectWatches);
174 :
175 16 : for (String projectName : cfg.getSubsections(PROJECT)) {
176 5 : cfg.unsetSection(PROJECT, projectName);
177 5 : }
178 :
179 : ListMultimap<String, String> notifyValuesByProject =
180 16 : MultimapBuilder.hashKeys().arrayListValues().build();
181 16 : for (Map.Entry<ProjectWatchKey, Set<NotifyConfig.NotifyType>> e : projectWatches.entrySet()) {
182 16 : NotifyValue notifyValue = NotifyValue.create(e.getKey().filter(), e.getValue());
183 16 : notifyValuesByProject.put(e.getKey().project().get(), notifyValue.toString());
184 16 : }
185 :
186 16 : for (Map.Entry<String, Collection<String>> e : notifyValuesByProject.asMap().entrySet()) {
187 16 : cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY, new ArrayList<>(e.getValue()));
188 16 : }
189 :
190 16 : return cfg;
191 : }
192 :
193 : private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>>
194 : immutableCopyOf(Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches) {
195 : ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> b =
196 151 : ImmutableMap.builder();
197 151 : projectWatches.entrySet().stream()
198 151 : .forEach(e -> b.put(e.getKey(), ImmutableSet.copyOf(e.getValue())));
199 151 : return b.build();
200 : }
201 :
202 : @AutoValue
203 18 : public abstract static class NotifyValue {
204 : @Nullable
205 : public static NotifyValue parse(
206 : Account.Id accountId,
207 : String project,
208 : String notifyValue,
209 : ValidationError.Sink validationErrorSink) {
210 18 : notifyValue = notifyValue.trim();
211 18 : int i = notifyValue.lastIndexOf('[');
212 18 : if (i < 0 || notifyValue.charAt(notifyValue.length() - 1) != ']') {
213 2 : validationErrorSink.error(
214 2 : ValidationError.create(
215 : WATCH_CONFIG,
216 2 : String.format(
217 : "Invalid project watch of account %d for project %s: %s",
218 2 : accountId.get(), project, notifyValue)));
219 2 : return null;
220 : }
221 18 : String filter = notifyValue.substring(0, i).trim();
222 18 : if (filter.isEmpty() || FILTER_ALL.equals(filter)) {
223 18 : filter = null;
224 : }
225 :
226 18 : Set<NotifyConfig.NotifyType> notifyTypes = EnumSet.noneOf(NotifyConfig.NotifyType.class);
227 18 : if (i + 1 < notifyValue.length() - 2) {
228 : for (String nt :
229 18 : Splitter.on(',')
230 18 : .trimResults()
231 18 : .splitToList(notifyValue.substring(i + 1, notifyValue.length() - 1))) {
232 18 : NotifyConfig.NotifyType notifyType =
233 18 : Enums.getIfPresent(NotifyConfig.NotifyType.class, nt).orNull();
234 18 : if (notifyType == null) {
235 1 : validationErrorSink.error(
236 1 : ValidationError.create(
237 : WATCH_CONFIG,
238 1 : String.format(
239 : "Invalid notify type %s in project watch "
240 : + "of account %d for project %s: %s",
241 1 : nt, accountId.get(), project, notifyValue)));
242 1 : continue;
243 : }
244 18 : notifyTypes.add(notifyType);
245 18 : }
246 : }
247 18 : return create(filter, notifyTypes);
248 : }
249 :
250 : public static NotifyValue create(
251 : @Nullable String filter, Collection<NotifyConfig.NotifyType> notifyTypes) {
252 18 : return new AutoValue_ProjectWatches_NotifyValue(
253 18 : Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes));
254 : }
255 :
256 : public abstract @Nullable String filter();
257 :
258 : public abstract ImmutableSet<NotifyConfig.NotifyType> notifyTypes();
259 :
260 : @Override
261 : public final String toString() {
262 18 : List<NotifyConfig.NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
263 18 : StringBuilder notifyValue = new StringBuilder();
264 18 : notifyValue.append(firstNonNull(filter(), FILTER_ALL)).append(" [");
265 18 : Joiner.on(", ").appendTo(notifyValue, notifyTypes);
266 18 : notifyValue.append("]");
267 18 : return notifyValue.toString();
268 : }
269 : }
270 : }
|