blob: cbf47c5ef965bcfdcc3fe43fc7924851c8118858 [file] [log] [blame]
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.mail.send;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.NotifyConfig;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers.WatcherList;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.GroupBackedUser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class ProjectWatch {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
protected final EmailArguments args;
protected final ProjectState projectState;
protected final Project.NameKey project;
protected final ChangeData changeData;
public ProjectWatch(
EmailArguments args,
Project.NameKey project,
ProjectState projectState,
ChangeData changeData) {
this.args = args;
this.project = project;
this.projectState = projectState;
this.changeData = changeData;
}
/** Returns all watchers that are relevant */
public final Watchers getWatchers(
NotifyConfig.NotifyType type, boolean includeWatchersFromNotifyConfig) {
Watchers matching = new Watchers();
Set<Account.Id> projectWatchers = new HashSet<>();
for (AccountState a : args.accountQueryProvider.get().byWatchedProject(project)) {
Account.Id accountId = a.account().id();
for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
a.projectWatches().entrySet()) {
if (project.equals(e.getKey().project())
&& add(matching, accountId, e.getKey(), e.getValue(), type)) {
// We only want to prevent matching All-Projects if this filter hits
projectWatchers.add(accountId);
}
}
}
for (AccountState a : args.accountQueryProvider.get().byWatchedProject(args.allProjectsName)) {
for (Map.Entry<ProjectWatchKey, ImmutableSet<NotifyConfig.NotifyType>> e :
a.projectWatches().entrySet()) {
if (args.allProjectsName.equals(e.getKey().project())) {
Account.Id accountId = a.account().id();
if (!projectWatchers.contains(accountId)) {
add(matching, accountId, e.getKey(), e.getValue(), type);
}
}
}
}
if (!includeWatchersFromNotifyConfig) {
return matching;
}
for (ProjectState state : projectState.tree()) {
for (NotifyConfig nc : state.getConfig().getNotifySections().values()) {
if (nc.isNotify(type)) {
try {
add(matching, state.getNameKey(), nc);
} catch (QueryParseException e) {
logger.atInfo().log(
"Project %s has invalid notify %s filter \"%s\": %s",
state.getName(), nc.getName(), nc.getFilter(), e.getMessage());
}
}
}
}
return matching;
}
public static class Watchers {
static class WatcherList {
protected final Set<Account.Id> accounts = new HashSet<>();
protected final Set<Address> emails = new HashSet<>();
private static WatcherList union(WatcherList... others) {
WatcherList union = new WatcherList();
for (WatcherList other : others) {
union.accounts.addAll(other.accounts);
union.emails.addAll(other.emails);
}
return union;
}
}
protected final WatcherList to = new WatcherList();
protected final WatcherList cc = new WatcherList();
protected final WatcherList bcc = new WatcherList();
WatcherList all() {
return WatcherList.union(to, cc, bcc);
}
WatcherList list(NotifyConfig.Header header) {
switch (header) {
case TO:
return to;
case CC:
return cc;
default:
case BCC:
return bcc;
}
}
}
private void add(Watchers matching, Project.NameKey projectName, NotifyConfig nc)
throws QueryParseException {
logger.atFine().log("Checking watchers for notify config %s from project %s", nc, projectName);
for (GroupReference groupRef : nc.getGroups()) {
CurrentUser user = new GroupBackedUser(ImmutableSet.of(groupRef.getUUID()));
if (filterMatch(user, nc.getFilter())) {
deliverToMembers(matching.list(nc.getHeader()), groupRef.getUUID());
logger.atFine().log("Added watchers for group %s", groupRef);
} else {
logger.atFine().log("The filter did not match for group %s; skip notification", groupRef);
}
}
if (!nc.getAddresses().isEmpty()) {
if (filterMatch(null, nc.getFilter())) {
matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
logger.atFine().log("Added watchers for these addresses: %s", nc.getAddresses());
} else {
logger.atFine().log(
"The filter did not match; skip notification for these addresses: %s",
nc.getAddresses());
}
}
}
private void deliverToMembers(WatcherList matching, AccountGroup.UUID startUUID) {
Set<AccountGroup.UUID> seen = new HashSet<>();
List<AccountGroup.UUID> q = new ArrayList<>();
seen.add(startUUID);
q.add(startUUID);
while (!q.isEmpty()) {
AccountGroup.UUID uuid = q.remove(q.size() - 1);
GroupDescription.Basic group = args.groupBackend.get(uuid);
if (group == null) {
logger.atFine().log("group %s not found, skip notification", uuid);
continue;
}
if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
// If the group has an email address, do not expand membership.
matching.emails.add(Address.create(group.getEmailAddress()));
logger.atFine().log(
"notify group email address %s; skip expanding to members", group.getEmailAddress());
continue;
}
if (!(group instanceof GroupDescription.Internal)) {
// Non-internal groups cannot be expanded by the server.
logger.atFine().log("group %s is not an internal group, skip notification", uuid);
continue;
}
logger.atFine().log("adding the members of group %s as watchers", uuid);
GroupDescription.Internal ig = (GroupDescription.Internal) group;
matching.accounts.addAll(ig.getMembers());
for (AccountGroup.UUID m : ig.getSubgroups()) {
if (seen.add(m)) {
q.add(m);
}
}
}
}
private boolean add(
Watchers matching,
Account.Id accountId,
ProjectWatchKey key,
Set<NotifyConfig.NotifyType> watchedTypes,
NotifyConfig.NotifyType type) {
logger.atFine().log("Checking project watch %s of account %s", key, accountId);
IdentifiedUser user = args.identifiedUserFactory.create(accountId);
try {
if (filterMatch(user, key.filter())) {
// If we are set to notify on this type, add the user.
// Otherwise, still return true to stop notifications for this user.
if (watchedTypes.contains(type)) {
matching.bcc.accounts.add(accountId);
}
logger.atFine().log("Added account %s as watcher", accountId);
return true;
}
logger.atFine().log("The filter did not match for account %s; skip notification", accountId);
} catch (QueryParseException e) {
// Ignore broken filter expressions.
logger.atInfo().log(
"Account %s has invalid filter in project watch %s: %s", accountId, key, e.getMessage());
}
return false;
}
private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
WatcherChangeQueryBuilder qb;
Predicate<ChangeData> p = null;
if (user == null) {
qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), args.anonymousUser.get());
} else {
qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), user);
p = qb.isVisible();
}
qb.forceAccountVisibilityCheck();
if (filter != null) {
Predicate<ChangeData> filterPredicate = qb.parse(filter);
if (p == null) {
p = filterPredicate;
} else {
p = Predicate.and(filterPredicate, p);
}
}
return p == null || p.asMatchable().match(changeData);
}
private static class WatcherChangeQueryBuilder extends ChangeQueryBuilder {
private WatcherChangeQueryBuilder(Arguments args) {
super(args);
}
public static WatcherChangeQueryBuilder asUser(ChangeQueryBuilder other, CurrentUser user) {
return new WatcherChangeQueryBuilder(other.getArgs().asUser(user));
}
@Override
protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
if (query.startsWith("refs/")) {
return ref(query);
}
// Adapt the capacity of this list when adding more default predicates.
List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
predicates.add(file(query));
try {
predicates.add(label(query));
} catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
// Skip.
}
predicates.add(commit(query));
predicates.add(message(query));
predicates.add(comment(query));
predicates.add(projects(query));
predicates.add(ref(query));
predicates.add(branch(query));
predicates.add(topic(query));
// Adapt the capacity of the "predicates" list when adding more default
// predicates.
return Predicate.or(predicates);
}
}
}