| // 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.entities.Account; | |
| import com.google.gerrit.entities.NotifyConfig; | |
| import com.google.gerrit.entities.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 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<NotifyConfig.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<NotifyConfig.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<NotifyConfig.NotifyType>> parse( | |
| Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) { | |
| Map<ProjectWatchKey, Set<NotifyConfig.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(Project.nameKey(projectName), notifyValue.filter()); | |
| if (!projectWatches.containsKey(key)) { | |
| projectWatches.put(key, EnumSet.noneOf(NotifyConfig.NotifyType.class)); | |
| } | |
| projectWatches.get(key).addAll(notifyValue.notifyTypes()); | |
| } | |
| } | |
| return immutableCopyOf(projectWatches); | |
| } | |
| public Config save(Map<ProjectWatchKey, Set<NotifyConfig.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<NotifyConfig.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<NotifyConfig.NotifyType>> | |
| immutableCopyOf(Map<ProjectWatchKey, Set<NotifyConfig.NotifyType>> projectWatches) { | |
| ImmutableMap.Builder<ProjectWatchKey, ImmutableSet<NotifyConfig.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 { | |
| @Nullable | |
| 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( | |
| ValidationError.create( | |
| 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<NotifyConfig.NotifyType> notifyTypes = EnumSet.noneOf(NotifyConfig.NotifyType.class); | |
| if (i + 1 < notifyValue.length() - 2) { | |
| for (String nt : | |
| Splitter.on(',') | |
| .trimResults() | |
| .splitToList(notifyValue.substring(i + 1, notifyValue.length() - 1))) { | |
| NotifyConfig.NotifyType notifyType = | |
| Enums.getIfPresent(NotifyConfig.NotifyType.class, nt).orNull(); | |
| if (notifyType == null) { | |
| validationErrorSink.error( | |
| ValidationError.create( | |
| 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<NotifyConfig.NotifyType> notifyTypes) { | |
| return new AutoValue_ProjectWatches_NotifyValue( | |
| Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes)); | |
| } | |
| public abstract @Nullable String filter(); | |
| public abstract ImmutableSet<NotifyConfig.NotifyType> notifyTypes(); | |
| @Override | |
| public final String toString() { | |
| List<NotifyConfig.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(); | |
| } | |
| } | |
| } |