LCOV - code coverage report
Current view: top level - server/account - ProjectWatches.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 81 82 98.8 %
Date: 2022-11-19 15:00:39 Functions: 13 13 100.0 %

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

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