Line data Source code
1 : // Copyright (C) 2013 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.mail.send;
16 :
17 : import com.google.common.base.Strings;
18 : import com.google.common.collect.ImmutableSet;
19 : import com.google.common.flogger.FluentLogger;
20 : import com.google.gerrit.entities.Account;
21 : import com.google.gerrit.entities.AccountGroup;
22 : import com.google.gerrit.entities.Address;
23 : import com.google.gerrit.entities.GroupDescription;
24 : import com.google.gerrit.entities.GroupReference;
25 : import com.google.gerrit.entities.NotifyConfig;
26 : import com.google.gerrit.entities.Project;
27 : import com.google.gerrit.index.query.Predicate;
28 : import com.google.gerrit.index.query.QueryParseException;
29 : import com.google.gerrit.server.CurrentUser;
30 : import com.google.gerrit.server.IdentifiedUser;
31 : import com.google.gerrit.server.account.AccountState;
32 : import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
33 : import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
34 : import com.google.gerrit.server.project.ProjectState;
35 : import com.google.gerrit.server.query.change.ChangeData;
36 : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
37 : import com.google.gerrit.server.query.change.GroupBackedUser;
38 : import java.util.ArrayList;
39 : import java.util.HashSet;
40 : import java.util.List;
41 : import java.util.Map;
42 : import java.util.Set;
43 :
44 : public class ProjectWatch {
45 103 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
46 :
47 : protected final EmailArguments args;
48 : protected final ProjectState projectState;
49 : protected final Project.NameKey project;
50 : protected final ChangeData changeData;
51 :
52 : public ProjectWatch(
53 : EmailArguments args,
54 : Project.NameKey project,
55 : ProjectState projectState,
56 103 : ChangeData changeData) {
57 103 : this.args = args;
58 103 : this.project = project;
59 103 : this.projectState = projectState;
60 103 : this.changeData = changeData;
61 103 : }
62 :
63 : /** Returns all watchers that are relevant */
64 : public final Watchers getWatchers(
65 : NotifyConfig.NotifyType type, boolean includeWatchersFromNotifyConfig) {
66 103 : Watchers matching = new Watchers();
67 103 : Set<Account.Id> projectWatchers = new HashSet<>();
68 :
69 103 : for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
70 7 : Account.Id accountId = a.account().id();
71 : for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
72 7 : a.projectWatches().entrySet()) {
73 7 : if (project.equals(e.getKey().project())
74 7 : && add(matching, accountId, e.getKey(), e.getValue(), type)) {
75 : // We only want to prevent matching All-Projects if this filter hits
76 7 : projectWatchers.add(accountId);
77 : }
78 7 : }
79 7 : }
80 :
81 103 : for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
82 : for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
83 2 : a.projectWatches().entrySet()) {
84 2 : if (args.allProjectsName.equals(e.getKey().project())) {
85 2 : Account.Id accountId = a.account().id();
86 2 : if (!projectWatchers.contains(accountId)) {
87 2 : add(matching, accountId, e.getKey(), e.getValue(), type);
88 : }
89 : }
90 2 : }
91 2 : }
92 :
93 103 : if (!includeWatchersFromNotifyConfig) {
94 18 : return matching;
95 : }
96 :
97 103 : for (ProjectState state : projectState.tree()) {
98 103 : for (NotifyConfig nc : state.getConfig().getNotifySections().values()) {
99 2 : if (nc.isNotify(type)) {
100 : try {
101 2 : add(matching, state.getNameKey(), nc);
102 0 : } catch (QueryParseException e) {
103 0 : logger.atInfo().log(
104 : "Project %s has invalid notify %s filter \"%s\": %s",
105 0 : state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
106 2 : }
107 : }
108 2 : }
109 103 : }
110 :
111 103 : return matching;
112 : }
113 :
114 103 : public static class Watchers {
115 103 : static class WatcherList {
116 103 : protected final Set<Account.Id> accounts = new HashSet<>();
117 103 : protected final Set<Address> emails = new HashSet<>();
118 :
119 : private static WatcherList union(WatcherList... others) {
120 0 : WatcherList union = new WatcherList();
121 0 : for (WatcherList other : others) {
122 0 : union.accounts.addAll(other.accounts);
123 0 : union.emails.addAll(other.emails);
124 : }
125 0 : return union;
126 : }
127 : }
128 :
129 103 : protected final WatcherList to = new WatcherList();
130 103 : protected final WatcherList cc = new WatcherList();
131 103 : protected final WatcherList bcc = new WatcherList();
132 :
133 : WatcherList all() {
134 0 : return WatcherList.union(to, cc, bcc);
135 : }
136 :
137 : WatcherList list(NotifyConfig.Header header) {
138 2 : switch (header) {
139 : case TO:
140 1 : return to;
141 : case CC:
142 1 : return cc;
143 : default:
144 : case BCC:
145 1 : return bcc;
146 : }
147 : }
148 : }
149 :
150 : private void add(Watchers matching, Project.NameKey projectName, NotifyConfig nc)
151 : throws QueryParseException {
152 2 : logger.atFine().log("Checking watchers for notify config %s from project %s", nc, projectName);
153 2 : for (GroupReference groupRef : nc.getGroups()) {
154 0 : CurrentUser user = new GroupBackedUser(ImmutableSet.of(groupRef.getUUID()));
155 0 : if (filterMatch(user, nc.getFilter())) {
156 0 : deliverToMembers(matching.list(nc.getHeader()), groupRef.getUUID());
157 0 : logger.atFine().log("Added watchers for group %s", groupRef);
158 : } else {
159 0 : logger.atFine().log("The filter did not match for group %s; skip notification", groupRef);
160 : }
161 0 : }
162 :
163 2 : if (!nc.getAddresses().isEmpty()) {
164 2 : if (filterMatch(null, nc.getFilter())) {
165 2 : matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
166 2 : logger.atFine().log("Added watchers for these addresses: %s", nc.getAddresses());
167 : } else {
168 1 : logger.atFine().log(
169 : "The filter did not match; skip notification for these addresses: %s",
170 1 : nc.getAddresses());
171 : }
172 : }
173 2 : }
174 :
175 : private void deliverToMembers(WatcherList matching, AccountGroup.UUID startUUID) {
176 0 : Set<AccountGroup.UUID> seen = new HashSet<>();
177 0 : List<AccountGroup.UUID> q = new ArrayList<>();
178 :
179 0 : seen.add(startUUID);
180 0 : q.add(startUUID);
181 :
182 0 : while (!q.isEmpty()) {
183 0 : AccountGroup.UUID uuid = q.remove(q.size() - 1);
184 0 : GroupDescription.Basic group = args.groupBackend.get(uuid);
185 0 : if (group == null) {
186 0 : logger.atFine().log("group %s not found, skip notification", uuid);
187 0 : continue;
188 : }
189 0 : if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
190 : // If the group has an email address, do not expand membership.
191 0 : matching.emails.add(Address.create(group.getEmailAddress()));
192 0 : logger.atFine().log(
193 0 : "notify group email address %s; skip expanding to members", group.getEmailAddress());
194 0 : continue;
195 : }
196 :
197 0 : if (!(group instanceof GroupDescription.Internal)) {
198 : // Non-internal groups cannot be expanded by the server.
199 0 : logger.atFine().log("group %s is not an internal group, skip notification", uuid);
200 0 : continue;
201 : }
202 :
203 0 : logger.atFine().log("adding the members of group %s as watchers", uuid);
204 0 : GroupDescription.Internal ig = (GroupDescription.Internal) group;
205 0 : matching.accounts.addAll(ig.getMembers());
206 0 : for (AccountGroup.UUID m : ig.getSubgroups()) {
207 0 : if (seen.add(m)) {
208 0 : q.add(m);
209 : }
210 0 : }
211 0 : }
212 0 : }
213 :
214 : private boolean add(
215 : Watchers matching,
216 : Account.Id accountId,
217 : ProjectWatchKey key,
218 : Set<NotifyConfig.NotifyType> watchedTypes,
219 : NotifyConfig.NotifyType type) {
220 8 : logger.atFine().log("Checking project watch %s of account %s", key, accountId);
221 :
222 8 : IdentifiedUser user = args.identifiedUserFactory.create(accountId);
223 : try {
224 8 : if (filterMatch(user, key.filter())) {
225 : // If we are set to notify on this type, add the user.
226 : // Otherwise, still return true to stop notifications for this user.
227 8 : if (watchedTypes.contains(type)) {
228 8 : matching.bcc.accounts.add(accountId);
229 : }
230 8 : logger.atFine().log("Added account %s as watcher", accountId);
231 8 : return true;
232 : }
233 1 : logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
234 0 : } catch (QueryParseException e) {
235 : // Ignore broken filter expressions.
236 0 : logger.atInfo().log(
237 0 : "Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
238 1 : }
239 1 : return false;
240 : }
241 :
242 : private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
243 : ChangeQueryBuilder qb;
244 9 : Predicate<ChangeData> p = null;
245 :
246 9 : if (user == null) {
247 2 : qb = args.queryBuilder.get().asUser(args.anonymousUser.get());
248 : } else {
249 8 : qb = args.queryBuilder.get().asUser(user);
250 8 : p = qb.isVisible();
251 : }
252 :
253 9 : if (filter != null) {
254 2 : Predicate<ChangeData> filterPredicate = qb.parse(filter);
255 2 : if (p == null) {
256 2 : p = filterPredicate;
257 : } else {
258 1 : p = Predicate.and(filterPredicate, p);
259 : }
260 : }
261 9 : return p == null || p.asMatchable().match(changeData);
262 : }
263 : }
|