code: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You 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 org.apache.skywalking.oap.server.storage.plugin.elasticsearch.base;
  
  import java.util.HashMap;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.stream.Collectors;
  import lombok.EqualsAndHashCode;
  import org.apache.skywalking.library.elasticsearch.response.Mappings;
  import org.apache.skywalking.oap.server.core.Const;
  import org.apache.skywalking.oap.server.library.client.elasticsearch.ElasticSearchClient;
  import org.apache.skywalking.oap.server.library.util.CollectionUtils;
  
  public class IndexStructures {
      private final Map<String, Fields> mappingStructures;
      private final Map<String, UpdatableIndexSettings> indexSettingStructures;
  
      public IndexStructures() {
          this.mappingStructures = new HashMap<>();
          this.indexSettingStructures = new HashMap<>();
      }
  
      public Mappings getMapping(String tableName) {
          Map<String, Object> properties =
              mappingStructures.containsKey(tableName) ?
                  mappingStructures.get(tableName).properties : new HashMap<>();
          Mappings.Source source =
                      mappingStructures.containsKey(tableName) ?
                          mappingStructures.get(tableName).source : new Mappings.Source();
          return Mappings.builder()
                         .type(ElasticSearchClient.TYPE)
                         .properties(properties)
                         .source(source)
                         .build();
      }
  
      /**
       * Add or append mapping/settings when the current structures don't contain the input structure or having
       * new fields in it.
       */
      public void putStructure(String tableName, Mappings mapping, Map<String, Object> settings) {
          if (CollectionUtils.isNotEmpty(settings) && Objects.nonNull(settings.get("index"))) {
              this.indexSettingStructures.putIfAbsent(tableName, new UpdatableIndexSettings(
                  (Map<String, Object>) settings.get("index")));
          }
          if (Objects.isNull(mapping)
              || Objects.isNull(mapping.getProperties())
              || mapping.getProperties().isEmpty()) {
              return;
          }
          Fields fields = new Fields(mapping);
          if (mappingStructures.containsKey(tableName)) {
              mappingStructures.get(tableName).appendNewFields(fields);
          } else {
              mappingStructures.put(tableName, fields);
          }
      }
  
      /**
       * Returns mappings with fields that not exist in the input mappings.
       * The input mappings should be history mapping from current index.
       * Do not return _source config to avoid current index update conflict.
       */
      public Mappings diffMappings(String tableName, Mappings mappings) {
          if (!mappingStructures.containsKey(tableName)) {
              return new Mappings();
          }
          Map<String, Object> diffProperties =
              mappingStructures.get(tableName).diffFields(new Fields(mappings));
          return Mappings.builder()
                         .type(ElasticSearchClient.TYPE)
                         .properties(diffProperties)
                         .build();
      }
  
      /**
       * Returns true when the current structures already contains the properties of the input
       * mappings.
       */
      public boolean containsMapping(String tableName, Mappings mappings) {
          if (Objects.isNull(mappings) ||
              CollectionUtils.isEmpty(mappings.getProperties())) {
              return true;
          }
  
          return mappingStructures.containsKey(tableName)
              && mappingStructures.get(tableName)
                                  .containsAllFields(new Fields(mappings));
      }
  
      /**
       * Check whether all the fields are already defined regardless of the field types.
       * When OAP runs in no-init mode, it doesn't care about the field types, it just
       * cares about whether the data can be ingested without reporting error.
       * OAP running in init mode should take care of the field types.
       */
      public boolean containsFieldNames(String tableName, Mappings mappings) {
          if (Objects.isNull(mappings) ||
              CollectionUtils.isEmpty(mappings.getProperties())) {
              return true;
          }
  
          return mappingStructures.containsKey(tableName) &&
                  mappingStructures.get(tableName)
                          .containsAllFieldNames(new Fields(mappings));
      }
  
      /**
       * Returns true when the current index setting equals the input.
       */
      public boolean compareIndexSetting(String tableName, Map<String, Object> settings) {
          if ((CollectionUtils.isEmpty(settings) || CollectionUtils.isEmpty((Map) settings.get("index")))
              && Objects.isNull(indexSettingStructures.get(tableName))) {
              return true;
          }
  
          return indexSettingStructures.containsKey(tableName)
              && indexSettingStructures.get(tableName).
                                       equals(new UpdatableIndexSettings((Map<String, Object>) settings.get("index")));
      }
  
      /**
       * The mapping properties of the template or index.
       */
      public static class Fields {
          private final Map<String, Object> properties;
          private final Mappings.Source source;
  
          private Fields(Mappings mapping) {
              this.properties = mapping.getProperties();
              this.source = mapping.getSource();
          }
  
          /**
           * Returns ture when the input fields have already been stored in the properties.
           */
          private boolean containsAllFields(Fields fields) {
              if (this.properties.size() < fields.properties.size()) {
                  return false;
              }
              boolean isContains = fields.properties.entrySet().stream()
                      .allMatch(item -> Objects.equals(properties.get(item.getKey()), item.getValue()));
              if (!isContains) {
                  return false;
              }
              Set<String> inputExcludes = fields.source.getExcludes();
              Set<String> excludes = source.getExcludes();
              //need to add new excludes
              if (!excludes.containsAll(inputExcludes)) {
                  return false;
              }
              //need to delete existing excludes
              for (String p : fields.properties.keySet()) {
                  if (!inputExcludes.contains(p) && excludes.contains(p)) {
                      return false;
                  }
              }
              return true;
          }
  
          private boolean containsAllFieldNames(Fields fields) {
              if (this.properties.size() < fields.properties.size()) {
                  return false;
              }
              return this.properties.keySet().containsAll(fields.properties.keySet());
          }
  
          /**
           * Append new fields and update.
           * Properties combine input and exist, update property's attribute, won't remove old one.
           * If the existed `excludes` contains a not existing property, the excluded field would be removed.
           */
          private void appendNewFields(Fields fields) {
              properties.putAll(fields.properties);
              Set<String> inputExcludes = fields.source.getExcludes();
              Set<String> excludes = source.getExcludes();
              excludes.addAll(inputExcludes);
              fields.properties.keySet().forEach(p -> {
                  if (!inputExcludes.contains(p) && excludes.contains(p)) {
                      excludes.remove(p);
                  }
              });
          }
  
          /**
           * Returns the properties that not exist in the input fields.
           */
          private Map<String, Object> diffFields(Fields fields) {
              return this.properties.entrySet().stream()
                                    .filter(e -> !fields.properties.containsKey(e.getKey()))
                                    .collect(Collectors.toMap(
                                        Map.Entry::getKey,
                                        Map.Entry::getValue
                                    ));
          }
      }
  
      /**
       * The index settings structure which only includes needs to compare for update fields
       */
      @EqualsAndHashCode
      public static class UpdatableIndexSettings {
          private final String replicas;
          private final String shards;
  
          public UpdatableIndexSettings(Map<String, Object> indexSettings) {
              this.replicas = String.valueOf(indexSettings.getOrDefault("number_of_replicas", Const.EMPTY_STRING));
              this.shards = String.valueOf(indexSettings.getOrDefault("number_of_shards", Const.EMPTY_STRING));
          }
      }
  }
  