All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.account.ProjectWatches Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.
 *
 * 

This is a low-level API. Read/write of project watches in a user branch should be done through * {@link AccountsUpdate} or {@link AccountConfig}. * *

The config file has one 'project' section for all project watches of a project. * *

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 * []}: * *

 *   [project "foo"]
 *     notify = * [ALL_COMMENTS]
 *     notify = branch:master [ALL_COMMENTS, NEW_PATCHSETS]
 *     notify = branch:master owner:self [SUBMITTED_CHANGES]
 * 
* *

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. * *

For watch configurations that notify on no event the list of notify types is empty: * *

 *   [project "foo"]
 *     notify = branch:master []
 * 
* *

Unknown notify types are ignored and removed on save. * *

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> 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> 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. * *

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. * *

A project watch on the {@code All-Projects} project applies for all projects unless the * project has a matching project watch. * *

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. * *

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> parse( Account.Id accountId, Config cfg, ValidationError.Sink validationErrorSink) { Map> 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> projectWatches) { this.projectWatches = immutableCopyOf(projectWatches); for (String projectName : cfg.getSubsections(PROJECT)) { cfg.unsetSection(PROJECT, projectName); } ListMultimap notifyValuesByProject = MultimapBuilder.hashKeys().arrayListValues().build(); for (Map.Entry> e : projectWatches.entrySet()) { NotifyValue notifyValue = NotifyValue.create(e.getKey().filter(), e.getValue()); notifyValuesByProject.put(e.getKey().project().get(), notifyValue.toString()); } for (Map.Entry> e : notifyValuesByProject.asMap().entrySet()) { cfg.setStringList(PROJECT, e.getKey(), KEY_NOTIFY, new ArrayList<>(e.getValue())); } return cfg; } private static ImmutableMap> immutableCopyOf( Map> projectWatches) { ImmutableMap.Builder> 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 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 notifyTypes) { return new AutoValue_ProjectWatches_NotifyValue( Strings.emptyToNull(filter), Sets.immutableEnumSet(notifyTypes)); } public abstract @Nullable String filter(); public abstract ImmutableSet notifyTypes(); @Override public String toString() { List 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(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy