Files
gerrit/java/com/google/gerrit/server/account/ProjectWatches.java
David Pursehouse e1e0ae5157 Prefer requireNonNull from Java API over Guava's checkNotNull
Most replacements are a trivial one-to-one replacement of the call to
checkNotNull with requireNonNull, however there are some calls that
make use of checkNotNull's variable argument list and formatting
parameters. Replace these with String.format wrapped in a supplier.

Using the Java API rather than Guava means we can get rid of the
custom implementation, and Edwin's TODO, in AccessSection.

Change-Id: I1d4a6b342e3544abdbba4f05ac4308d223b6768b
2018-10-16 18:34:13 +09:00

278 lines
10 KiB
Java

// Copyright (C) 2016 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.account;
import static com.google.common.base.MoreObjects.firstNonNull;
import static java.util.Objects.requireNonNull;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.ValidationError;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
/**
* Parses/writes project watches from/to a {@link Config} file.
*
* <p>This is a low-level API. Read/write of project watches in a user branch should be done through
* {@link AccountsUpdate} or {@link AccountConfig}.
*
* <p>The config file has one 'project' section for all project watches of a project.
*
* <p>The project name is used as subsection name and the filters with the notify types that decide
* for which events email notifications should be sent are represented as 'notify' values in the
* subsection. A 'notify' value is formatted as {@code <filter>
* [<comma-separated-list-of-notify-types>]}:
*
* <pre>
* [project "foo"]
* notify = * [ALL_COMMENTS]
* notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
* notify = branch:master owner:self [SUBMITTED_CHANGES]
* </pre>
*
* <p>If two notify values in the same subsection have the same filter they are merged on the next
* save, taking the union of the notify types.
*
* <p>For watch configurations that notify on no event the list of notify types is empty:
*
* <pre>
* [project "foo"]
* notify = branch:master []
* </pre>
*
* <p>Unknown notify types are ignored and removed on save.
*
* <p>The project watches are lazily parsed.
*/
public class ProjectWatches {
@AutoValue
public abstract static class ProjectWatchKey {
public static ProjectWatchKey create(Project.NameKey project, @Nullable String filter) {
return new AutoValue_ProjectWatches_ProjectWatchKey(project, Strings.emptyToNull(filter));
}
public abstract Project.NameKey project();
public abstract @Nullable String filter();
}
public enum NotifyType {
// sort by name, except 'ALL' which should stay last
ABANDONED_CHANGES,
ALL_COMMENTS,
NEW_CHANGES,
NEW_PATCHSETS,
SUBMITTED_CHANGES,
ALL
}
public static final String FILTER_ALL = "*";
public static final String WATCH_CONFIG = "watch.config";
public static final String PROJECT = "project";
public static final String KEY_NOTIFY = "notify";
private final Account.Id accountId;
private final Config cfg;
private final ValidationError.Sink validationErrorSink;
private ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches;
ProjectWatches(Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
this.accountId = requireNonNull(accountId, "accountId");
this.cfg = requireNonNull(cfg, "cfg");
this.validationErrorSink = requireNonNull(validationErrorSink, "validationErrorSink");
}
public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
if (projectWatches == null) {
parse();
}
return projectWatches;
}
public void parse() {
projectWatches = parse(accountId, cfg, validationErrorSink);
}
/**
* Parses project watches from the given config file and returns them as a map.
*
* <p>A project watch is defined on a project and has a filter to match changes for which the
* project watch should be applied. The project and the filter form the map key. The map value is
* a set of notify types that decide for which events email notifications should be sent.
*
* <p>A project watch on the {@code All-Projects} project applies for all projects unless the
* project has a matching project watch.
*
* <p>A project watch can have an empty set of notify types. An empty set of notify types means
* that no notification for matching changes should be set. This is different from no project
* watch as it overwrites matching project watches from the {@code All-Projects} project.
*
* <p>Since we must be able to differentiate a project watch with an empty set of notify types
* from no project watch we can't use a {@link Multimap} as return type.
*
* @param accountId the ID of the account for which the project watches should be parsed
* @param cfg the config file from which the project watches should be parsed
* @param validationErrorSink validation error sink
* @return the parsed project watches
*/
@VisibleForTesting
public static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> parse(
Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) {
Map<ProjectWatchKey, Set<NotifyType>> projectWatches = new HashMap<>();
for (String projectName : cfg.getSubsections(PROJECT)) {
String[] notifyValues = cfg.getStringList(PROJECT, projectName, KEY_NOTIFY);
for (String nv : notifyValues) {
if (Strings.isNullOrEmpty(nv)) {
continue;
}
NotifyValue notifyValue =
NotifyValue.parse(accountId, projectName, nv, validationErrorSink);
if (notifyValue == null) {
continue;
}
ProjectWatchKey key =
ProjectWatchKey.create(new Project.NameKey(projectName), notifyValue.filter());
if (!projectWatches.containsKey(key)) {
projectWatches.put(key, EnumSet.noneOf(NotifyType.class));
}
projectWatches.get(key).addAll(notifyValue.notifyTypes());
}
}
return immutableCopyOf(projectWatches);
}
public Config save(Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
this.projectWatches = immutableCopyOf(projectWatches);
for (String projectName : cfg.getSubsections(PROJECT)) {
cfg.unsetSection(PROJECT, projectName);
}
ListMultimap<String, String> notifyValuesByProject =
MultimapBuilder.hashKeys().arrayListValues().build();
for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e : projectWatches.entrySet()) {
NotifyValue notifyValue = NotifyValue.create(e.getKey().filter(), e.getValue());
notifyValuesByProject.put(e.getKey().project().get(), notifyValue.toString());
}
for (Map.Entry<String, Collection<String>> e : notifyValuesByProject.asMap().entrySet()) {
cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY, new ArrayList<>(e.getValue()));
}
return cfg;
}
private static ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> immutableCopyOf(
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyType>> b = ImmutableMap.builder();
projectWatches
.entrySet()
.stream()
.forEach(e -> b.put(e.getKey(), ImmutableSet.copyOf(e.getValue())));
return b.build();
}
@AutoValue
public abstract static class NotifyValue {
public static NotifyValue parse(
Account.Id accountId,
String project,
String notifyValue,
ValidationError.Sink validationErrorSink) {
notifyValue = notifyValue.trim();
int i = notifyValue.lastIndexOf('[');
if (i < 0 || notifyValue.charAt(notifyValue.length() - 1) != ']') {
validationErrorSink.error(
new ValidationError(
WATCH_CONFIG,
String.format(
"Invalid project watch of account %d for project %s: %s",
accountId.get(), project, notifyValue)));
return null;
}
String filter = notifyValue.substring(0, i).trim();
if (filter.isEmpty() || FILTER_ALL.equals(filter)) {
filter = null;
}
Set<NotifyType> notifyTypes = EnumSet.noneOf(NotifyType.class);
if (i + 1 < notifyValue.length() - 2) {
for (String nt :
Splitter.on(',')
.trimResults()
.splitToList(notifyValue.substring(i + 1, notifyValue.length() - 1))) {
NotifyType notifyType = Enums.getIfPresent(NotifyType.class, nt).orNull();
if (notifyType == null) {
validationErrorSink.error(
new ValidationError(
WATCH_CONFIG,
String.format(
"Invalid notify type %s in project watch "
+ "of account %d for project %s: %s",
nt, accountId.get(), project, notifyValue)));
continue;
}
notifyTypes.add(notifyType);
}
}
return create(filter, notifyTypes);
}
public static NotifyValue create(@Nullable String filter, Collection<NotifyType> notifyTypes) {
return new AutoValue_ProjectWatches_NotifyValue(
Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes));
}
public abstract @Nullable String filter();
public abstract ImmutableSet<NotifyType> notifyTypes();
@Override
public String toString() {
List<NotifyType> notifyTypes = new ArrayList<>(notifyTypes());
StringBuilder notifyValue = new StringBuilder();
notifyValue.append(firstNonNull(filter(), FILTER_ALL)).append(" [");
Joiner.on(", ").appendTo(notifyValue, notifyTypes);
notifyValue.append("]");
return notifyValue.toString();
}
}
}