blob: c3516dd1aaac76ac015e239d43de09976fd651ee [file] [log] [blame]
// Copyright (C) 2018 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.config;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.lib.Config;
/**
* This event is produced by {@link GerritServerConfigReloader} and forwarded to callers
* implementing {@link GerritConfigListener}.
*
* <p>The event intends to:
*
* <p>1. Help the callers figure out if any action should be taken, depending on which entries are
* updated in gerrit.config.
*
* <p>2. Provide the callers with a mechanism to accept/reject the entries of interest: {@link
* #accept(Set)}, {@link #accept(String)}, {@link #reject(Set)} (+ various overloaded versions of
* these)
*/
public class ConfigUpdatedEvent {
public static final ImmutableMultimap<UpdateResult, ConfigUpdateEntry> NO_UPDATES =
new ImmutableMultimap.Builder<UpdateResult, ConfigUpdateEntry>().build();
private final Config oldConfig;
private final Config newConfig;
public ConfigUpdatedEvent(Config oldConfig, Config newConfig) {
this.oldConfig = oldConfig;
this.newConfig = newConfig;
}
public Config getOldConfig() {
return this.oldConfig;
}
public Config getNewConfig() {
return this.newConfig;
}
private String getString(ConfigKey key, Config config) {
return config.getString(key.section(), key.subsection(), key.name());
}
public Multimap<UpdateResult, ConfigUpdateEntry> accept(ConfigKey entry) {
return accept(Collections.singleton(entry));
}
public ListMultimap<UpdateResult, ConfigUpdateEntry> accept(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.APPLIED);
}
public ListMultimap<UpdateResult, ConfigUpdateEntry> accept(String section) {
Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
entries.addAll(getEntriesFromSection(newConfig, section));
return createUpdate(entries, UpdateResult.APPLIED);
}
public ListMultimap<UpdateResult, ConfigUpdateEntry> reject(ConfigKey entry) {
return reject(Collections.singleton(entry));
}
public ListMultimap<UpdateResult, ConfigUpdateEntry> reject(Set<ConfigKey> entries) {
return createUpdate(entries, UpdateResult.REJECTED);
}
private static Set<ConfigKey> getEntriesFromSection(Config config, String section) {
Set<ConfigKey> res = new LinkedHashSet<>();
for (String name : config.getNames(section, true)) {
res.add(ConfigKey.create(section, name));
}
for (String sub : config.getSubsections(section)) {
for (String name : config.getNames(section, sub, true)) {
res.add(ConfigKey.create(section, sub, name));
}
}
return res;
}
private ListMultimap<UpdateResult, ConfigUpdateEntry> createUpdate(
Set<ConfigKey> entries, UpdateResult updateResult) {
ListMultimap<UpdateResult, ConfigUpdateEntry> updates = ArrayListMultimap.create();
entries.stream()
.filter(this::isValueUpdated)
.map(e -> new ConfigUpdateEntry(e, getString(e, oldConfig), getString(e, newConfig)))
.forEach(e -> updates.put(updateResult, e));
return updates;
}
public boolean isSectionUpdated(String section) {
Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
entries.addAll(getEntriesFromSection(newConfig, section));
return isEntriesUpdated(entries);
}
public boolean isValueUpdated(String section, String subsection, String name) {
return !Objects.equals(
oldConfig.getString(section, subsection, name),
newConfig.getString(section, subsection, name));
}
public boolean isValueUpdated(ConfigKey key) {
return isValueUpdated(key.section(), key.subsection(), key.name());
}
public boolean isValueUpdated(String section, String name) {
return isValueUpdated(section, null, name);
}
public boolean isEntriesUpdated(Set<ConfigKey> entries) {
for (ConfigKey entry : entries) {
if (isValueUpdated(entry.section(), entry.subsection(), entry.name())) {
return true;
}
}
return false;
}
public enum UpdateResult {
APPLIED,
REJECTED;
@Override
public String toString() {
return StringUtils.capitalize(name().toLowerCase(Locale.US));
}
}
public enum ConfigEntryType {
ADDED,
REMOVED,
MODIFIED,
UNMODIFIED
}
public static class ConfigUpdateEntry {
public final ConfigKey key;
public final String oldVal;
public final String newVal;
public ConfigUpdateEntry(ConfigKey key, String oldVal, String newVal) {
this.key = key;
this.oldVal = oldVal;
this.newVal = newVal;
}
/** Note: The toString() is used to format the output from @see ReloadConfig. */
@Override
public String toString() {
switch (getUpdateType()) {
case ADDED:
return String.format("+ %s = %s", key, newVal);
case MODIFIED:
return String.format("* %s = [%s => %s]", key, oldVal, newVal);
case REMOVED:
return String.format("- %s = %s", key, oldVal);
case UNMODIFIED:
return String.format(" %s = %s", key, newVal);
default:
throw new IllegalStateException("Unexpected UpdateType: " + getUpdateType().name());
}
}
public ConfigEntryType getUpdateType() {
if (oldVal == null && newVal != null) {
return ConfigEntryType.ADDED;
}
if (oldVal != null && newVal == null) {
return ConfigEntryType.REMOVED;
}
if (Objects.equals(oldVal, newVal)) {
return ConfigEntryType.UNMODIFIED;
}
return ConfigEntryType.MODIFIED;
}
}
}