com.google.gerrit.server.config.ConfigUpdatedEvent Maven / Gradle / Ivy
// 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}.
 *
 * The event intends to:
 *
 * 
1. Help the callers figure out if any action should be taken, depending on which entries are
 * updated in gerrit.config.
 *
 * 
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 NO_UPDATES =
      new ImmutableMultimap.Builder().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 accept(ConfigKey entry) {
    return accept(Collections.singleton(entry));
  }
  public ListMultimap accept(Set entries) {
    return createUpdate(entries, UpdateResult.APPLIED);
  }
  public ListMultimap accept(String section) {
    Set entries = getEntriesFromSection(oldConfig, section);
    entries.addAll(getEntriesFromSection(newConfig, section));
    return createUpdate(entries, UpdateResult.APPLIED);
  }
  public ListMultimap reject(ConfigKey entry) {
    return reject(Collections.singleton(entry));
  }
  public ListMultimap reject(Set entries) {
    return createUpdate(entries, UpdateResult.REJECTED);
  }
  private static Set getEntriesFromSection(Config config, String section) {
    Set 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 createUpdate(
      Set entries, UpdateResult updateResult) {
    ListMultimap 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 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 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() {
      return switch (getUpdateType()) {
        case ADDED -> String.format("+ %s = %s", key, newVal);
        case MODIFIED -> String.format("* %s = [%s => %s]", key, oldVal, newVal);
        case REMOVED -> String.format("- %s = %s", key, oldVal);
        case UNMODIFIED -> 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;
    }
  }
}