LCOV - code coverage report
Current view: top level - server/mail/send - ProjectWatch.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 76 121 62.8 %
Date: 2022-11-19 15:00:39 Functions: 9 12 75.0 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750