blob: 0b2c5ab386b5b1b6763738b302f148e60467eaaa [file] [log] [blame]
// Copyright (C) 2019 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.plugins.checks.db;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Streams;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerCreation;
import com.google.gerrit.plugins.checks.CheckerUpdate;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.api.BlockingCondition;
import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.Project;
import java.text.MessageFormat;
import java.util.Locale;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.StringUtils;
/**
* A basic property of a checker.
*
* <p>Each property knows how to read and write its value from/to a JGit {@link Config} file.
*/
enum CheckerConfigEntry {
/**
* The UUID of a checker. This property is equivalent to {@link Checker#getUuid()}.
*
* <p>This is a mandatory property.
*/
UUID("uuid") {
@Override
void readFromConfig(@Nullable CheckerUuid checkerUuid, Checker.Builder checker, Config config)
throws ConfigInvalidException {
String configUuid = config.getString(SECTION_NAME, null, super.keyName);
if (configUuid == null) {
throw new ConfigInvalidException(
String.format(
"%s.%s is not set in config file for checker %s",
SECTION_NAME, super.keyName, checkerUuid));
}
if (checkerUuid != null && !configUuid.equals(checkerUuid.get())) {
throw new ConfigInvalidException(
String.format(
"value of %s.%s=%s does not match expected checker UUID %s",
SECTION_NAME, super.keyName, configUuid, checkerUuid));
}
checker.setUuid(
CheckerUuid.tryParse(configUuid)
.orElseThrow(
() ->
new ConfigInvalidException(
String.format(
"invalid UUID in %s.%s=%s",
SECTION_NAME, super.keyName, configUuid))));
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
config.setString(SECTION_NAME, null, super.keyName, checkerCreation.getCheckerUuid().get());
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
// Do nothing. UUID is immutable.
}
},
/**
* The name of a checker. This property is equivalent to {@link Checker#getName()}.
*
* <p>It defaults to {@code null} if not set.
*/
NAME("name") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config) {
String name = config.getString(SECTION_NAME, null, super.keyName);
if (name != null) {
checker.setName(name);
}
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
// Do nothing. Name key will be set by updateConfigValue.
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getName()
.ifPresent(
name -> {
if (!Strings.isNullOrEmpty(name)) {
config.setString(SECTION_NAME, null, super.keyName, name);
} else {
config.unset(SECTION_NAME, null, super.keyName);
}
});
}
},
/**
* The description of a checker. This property is equivalent to {@link Checker#getDescription()}.
*
* <p>It defaults to {@code null} if not set.
*/
DESCRIPTION("description") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config) {
String description = config.getString(SECTION_NAME, null, super.keyName);
if (!Strings.isNullOrEmpty(description)) {
checker.setDescription(description);
}
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
// Do nothing. Description key will be set by updateConfigValue.
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getDescription()
.ifPresent(
description -> {
if (Strings.isNullOrEmpty(description)) {
config.unset(SECTION_NAME, null, super.keyName);
} else {
config.setString(SECTION_NAME, null, super.keyName, description);
}
});
}
},
/**
* The URL of a checker. This property is equivalent to {@link Checker#getUrl()}.
*
* <p>It defaults to {@code null} if not set.
*/
URL("url") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config) {
String url = config.getString(SECTION_NAME, null, super.keyName);
if (!Strings.isNullOrEmpty(url)) {
checker.setUrl(url);
}
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
// Do nothing. URL key will be set by updateConfigValue.
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getUrl()
.ifPresent(
url -> {
if (Strings.isNullOrEmpty(url)) {
config.unset(SECTION_NAME, null, super.keyName);
} else {
config.setString(SECTION_NAME, null, super.keyName, url);
}
});
}
},
/**
* The repository for which the checker applies. This property is equivalent to {@link
* Checker#getRepository()}.
*
* <p>This is a mandatory property.
*/
REPOSITORY("repository") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config)
throws ConfigInvalidException {
String repository = config.getString(SECTION_NAME, null, super.keyName);
// An empty repository is invalid in NoteDb; CheckerConfig will refuse to store it
if (repository == null) {
throw new ConfigInvalidException(
String.format(
"%s.%s is not set in config file for checker %s",
SECTION_NAME, super.keyName, checkerUuid));
}
checker.setRepository(Project.nameKey(repository));
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
String repository = checkerCreation.getRepository().get();
config.setString(SECTION_NAME, null, super.keyName, repository);
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getRepository()
.ifPresent(
repository -> config.setString(SECTION_NAME, null, super.keyName, repository.get()));
}
},
STATUS("status") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config)
throws ConfigInvalidException {
String value = config.getString(SECTION_NAME, null, super.keyName);
if (value == null) {
throw new ConfigInvalidException(
String.format(
"%s.%s is not set in config file for checker %s",
SECTION_NAME, super.keyName, checkerUuid));
}
try {
checker.setStatus(config.getEnum(SECTION_NAME, null, super.keyName, CheckerStatus.ENABLED));
} catch (IllegalArgumentException e) {
throw new ConfigInvalidException(e.getMessage());
}
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
// New checkers default to enabled.
config.setEnum(SECTION_NAME, null, super.keyName, CheckerStatus.ENABLED);
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getStatus()
.ifPresent(status -> config.setEnum(SECTION_NAME, null, super.keyName, status));
}
},
BLOCKING_CONDITIONS("blocking") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config)
throws ConfigInvalidException {
checker.setBlockingConditions(
getEnumSet(config, SECTION_NAME, super.keyName, BlockingCondition.values()));
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
// Do nothing. Blocking conditions will be set by updateConfigValue.
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getBlockingConditions()
.ifPresent(
blockingConditions ->
setEnumList(config, SECTION_NAME, null, super.keyName, blockingConditions));
}
},
QUERY("query") {
@Override
void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config) {
String value = config.getString(SECTION_NAME, null, super.keyName);
if (value != null) {
checker.setQuery(value);
}
}
@Override
void initNewConfig(Config config, CheckerCreation checkerCreation) {
config.setString(SECTION_NAME, null, super.keyName, "status:open");
}
@Override
void updateConfigValue(Config config, CheckerUpdate checkerUpdate) {
checkerUpdate
.getQuery()
.ifPresent(
query -> {
if (!Strings.isNullOrEmpty(query)) {
config.setString(SECTION_NAME, null, super.keyName, query);
} else {
config.unset(SECTION_NAME, null, super.keyName);
}
});
}
};
private static <T extends Enum<T>> ImmutableSortedSet<T> getEnumSet(
Config config, String section, String name, T[] all) throws ConfigInvalidException {
ImmutableSortedSet.Builder<T> enumBuilder = ImmutableSortedSet.naturalOrder();
for (String value : config.getStringList(section, null, name)) {
enumBuilder.add(resolveEnum(section, name, value, all));
}
return enumBuilder.build();
}
private static <T extends Enum<T>> T resolveEnum(
String section, String name, String value, T[] all) throws ConfigInvalidException {
// Match some resolution semantics of DefaultTypedConfigGetter#getEnum.
// TODO(dborowitz): Sure would be nice if Config exposed this logic (or getEnumList) so we
// didn't have to replicate it.
value = value.replace(' ', '_');
for (T e : all) {
if (StringUtils.equalsIgnoreCase(e.name(), value)) {
return e;
}
}
throw new ConfigInvalidException(
MessageFormat.format(JGitText.get().enumValueNotSupported2, section, name, value));
}
private static <T extends Enum<T>> void setEnumList(
Config config, String section, @Nullable String subsection, String name, Iterable<T> values) {
// Match semantics of Config#setEnum.
ImmutableList<String> strings =
Streams.stream(values)
.map(v -> v.name().toLowerCase(Locale.ROOT).replace('_', ' '))
.collect(toImmutableList());
config.setStringList(section, subsection, name, strings);
}
private static final String SECTION_NAME = "checker";
private final String keyName;
CheckerConfigEntry(String keyName) {
this.keyName = keyName;
}
/**
* Reads the corresponding property of this {@code CheckerConfigEntry} from the given {@code
* Config}. The read value is written to the corresponding property of {@code Checker.Builder}.
*
* @param checkerUuid the UUID of the checker (necessary for helpful error messages)
* @param checker the {@code Checker.Builder} whose property value should be set
* @param config the {@code Config} from which the value of the property should be read
* @throws ConfigInvalidException if the property has an unexpected value
*/
abstract void readFromConfig(CheckerUuid checkerUuid, Checker.Builder checker, Config config)
throws ConfigInvalidException;
/**
* Initializes the corresponding property of this {@code CheckerConfigEntry} in the given {@code
* Config}.
*
* <p>If the specified {@code CheckerCreation} has an entry for the property, that value is used.
* If not, the default value for the property is set. In any case, an existing entry for the
* property in the {@code Config} will be overwritten.
*
* @param config a new {@code Config}, typically without an entry for the property
* @param checkerCreation an {@code CheckerCreation} detailing the initial value of mandatory
* checker properties
*/
abstract void initNewConfig(Config config, CheckerCreation checkerCreation);
/**
* Updates the corresponding property of this {@code CheckerConfigEntry} in the given {@code
* Config} if the {@code CheckerUpdate} mentions a modification.
*
* <p>This call is a no-op if the {@code CheckerUpdate} doesn't contain a modification for the
* property.
*
* @param config a {@code Config} for which the property should be updated
* @param checkerUpdate an {@code CheckerUpdate} detailing the modifications on a checker
*/
abstract void updateConfigValue(Config config, CheckerUpdate checkerUpdate);
}