/*
 * 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.solr.schema;

import static org.apache.solr.core.SolrResourceLoader.informAware;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.CharFilterFactory;
import org.apache.lucene.analysis.TokenFilterFactory;
import org.apache.lucene.analysis.TokenizerFactory;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.ResourceLoaderAware;
import org.apache.lucene.util.Version;
import org.apache.solr.analysis.TokenizerChain;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrResponse;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.cloud.ZkController;
import org.apache.solr.cloud.ZkSolrResourceLoader;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.cloud.ClusterState;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.cloud.ZkCoreNodeProps;
import org.apache.solr.common.cloud.ZkStateReader;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ExecutorUtil;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SolrNamedThreadFactory;
import org.apache.solr.core.ConfigSetService;
import org.apache.solr.core.SolrConfig;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.rest.schema.FieldTypeXmlAdapter;
import org.apache.solr.util.DOMConfigNode;
import org.apache.solr.util.RTimer;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Solr-managed schema - non-user-editable, but can be mutable via internal and external REST API
 * requests.
 */
public final class ManagedIndexSchema extends IndexSchema {

  private final boolean isMutable;

  @Override
  public boolean isMutable() {
    return isMutable;
  }

  final String managedSchemaResourceName;

  int schemaZkVersion;

  final Object schemaUpdateLock;

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  /**
   * Constructs a schema using the specified resource name and stream.
   *
   * <p>By default, this follows the normal config path directory searching rules.
   *
   * @see org.apache.solr.core.SolrResourceLoader#openResource
   */
  ManagedIndexSchema(
      SolrConfig solrConfig,
      String name,
      ConfigSetService.ConfigResource is,
      boolean isMutable,
      String managedSchemaResourceName,
      int schemaZkVersion,
      Object schemaUpdateLock) {
    super(
        name,
        is,
        solrConfig.luceneMatchVersion,
        solrConfig.getResourceLoader(),
        solrConfig.getSubstituteProperties());
    this.isMutable = isMutable;
    this.managedSchemaResourceName = managedSchemaResourceName;
    this.schemaZkVersion = schemaZkVersion;
    this.schemaUpdateLock = schemaUpdateLock;
  }

  /**
   * Persist the schema to local storage or to ZooKeeper
   *
   * @param createOnly set to false to allow update of existing schema
   */
  public boolean persistManagedSchema(boolean createOnly) {
    if (loader instanceof ZkSolrResourceLoader) {
      return persistManagedSchemaToZooKeeper(createOnly);
    }
    // Persist locally
    Path managedSchemaFile = loader.getConfigPath().resolve(managedSchemaResourceName);
    try {
      Files.createDirectories(managedSchemaFile.getParent());
      try (Writer out = Files.newBufferedWriter(managedSchemaFile, StandardCharsets.UTF_8)) {
        persist(out);
      }
      log.info("Upgraded to managed schema at {}", managedSchemaFile);
    } catch (IOException e) {
      final String msg = "Error persisting managed schema " + managedSchemaFile;
      log.error(msg, e);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg, e);
    } finally {
      try {
        IOUtils.fsync(managedSchemaFile, false);
      } catch (IOException e) {
        final String msg = "Error syncing the managed schema file " + managedSchemaFile;
        log.error(msg, e);
      }
    }
    return true;
  }

  /**
   * Persists the managed schema to ZooKeeper using optimistic concurrency.
   *
   * <p>If createOnly is true, success is when the schema is created or if it previously existed.
   *
   * <p>If createOnly is false, success is when the schema is persisted - this will only happen if
   * schemaZkVersion matches the version in ZooKeeper.
   *
   * @return true on success
   */
  boolean persistManagedSchemaToZooKeeper(boolean createOnly) {
    final ZkSolrResourceLoader zkLoader = (ZkSolrResourceLoader) loader;
    final ZkController zkController = zkLoader.getZkController();
    final SolrZkClient zkClient = zkController.getZkClient();
    final String managedSchemaPath =
        zkLoader.getConfigSetZkPath() + "/" + managedSchemaResourceName;
    boolean success = true;
    boolean schemaChangedInZk = false;
    try {
      // Persist the managed schema
      StringWriter writer = new StringWriter();
      persist(writer);

      final byte[] data = writer.toString().getBytes(StandardCharsets.UTF_8);
      if (createOnly) {
        try {
          zkClient.create(managedSchemaPath, data, CreateMode.PERSISTENT, true);
          schemaZkVersion = 0;
          log.info("Created and persisted managed schema znode at {}", managedSchemaPath);
        } catch (KeeperException.NodeExistsException e) {
          // This is okay - do nothing and fall through
          log.info(
              "Managed schema znode at {} already exists - no need to create it",
              managedSchemaPath);
        }
      } else {
        try {
          // Assumption: the path exists
          Stat stat = zkClient.setData(managedSchemaPath, data, schemaZkVersion, true);
          schemaZkVersion = stat.getVersion();
          log.info(
              "Persisted managed schema version {}  at {}", schemaZkVersion, managedSchemaPath);
        } catch (KeeperException.BadVersionException e) {

          log.error(
              "Bad version when trying to persist schema using {} due to: ", schemaZkVersion, e);

          success = false;
          schemaChangedInZk = true;
        }
      }
    } catch (Exception e) {
      if (e instanceof InterruptedException) {
        Thread.currentThread().interrupt(); // Restore the interrupted status
      }
      final String msg = "Error persisting managed schema at " + managedSchemaPath;
      log.error(msg, e);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg, e);
    }
    if (schemaChangedInZk) {
      String msg =
          "Failed to persist managed schema at " + managedSchemaPath + " - version mismatch";
      log.info(msg);
      throw new SchemaChangedInZkException(ErrorCode.CONFLICT, msg + ", retry.");
    }
    return success;
  }

  /**
   * Block up to a specified maximum time until we see agreement on the schema version in ZooKeeper
   * across all replicas for a collection.
   */
  public static void waitForSchemaZkVersionAgreement(
      String collection,
      String localCoreNodeName,
      int schemaZkVersion,
      ZkController zkController,
      int maxWaitSecs) {
    RTimer timer = new RTimer();

    // get a list of active replica cores to query for the schema zk version (skipping this core of
    // course)
    List<GetZkSchemaVersionCallable> concurrentTasks = new ArrayList<>();
    for (String coreUrl : getActiveReplicaCoreUrls(zkController, collection, localCoreNodeName))
      concurrentTasks.add(new GetZkSchemaVersionCallable(coreUrl, schemaZkVersion, zkController));
    if (concurrentTasks.isEmpty()) return; // nothing to wait for ...

    if (log.isInfoEnabled()) {
      log.info(
          "Waiting up to {} secs for {} replicas to apply schema update version {} for collection {}",
          maxWaitSecs,
          concurrentTasks.size(),
          schemaZkVersion,
          collection);
    }

    // use an executor service to invoke schema zk version requests in parallel with a max wait time
    int poolSize = Math.min(concurrentTasks.size(), 10);
    ExecutorService parallelExecutor =
        ExecutorUtil.newMDCAwareFixedThreadPool(
            poolSize, new SolrNamedThreadFactory("managedSchemaExecutor"));
    try {
      List<Future<Integer>> results =
          parallelExecutor.invokeAll(concurrentTasks, maxWaitSecs, TimeUnit.SECONDS);

      // determine whether all replicas have the update
      List<String> failedList = null; // lazily init'd
      for (int f = 0; f < results.size(); f++) {
        int vers = -1;
        Future<Integer> next = results.get(f);
        if (next.isDone() && !next.isCancelled()) {
          // looks to have finished, but need to check the version value too
          try {
            vers = next.get();
          } catch (ExecutionException e) {
            // shouldn't happen since we checked isCancelled
          }
        }

        if (vers == -1) {
          String coreUrl = concurrentTasks.get(f).coreUrl;
          log.warn(
              "Core {} version mismatch! Expected {} but got {}", coreUrl, schemaZkVersion, vers);
          if (failedList == null) failedList = new ArrayList<>();
          failedList.add(coreUrl);
        }
      }

      // if any tasks haven't completed within the specified timeout, it's an error
      if (failedList != null)
        throw new SolrException(
            ErrorCode.SERVER_ERROR,
            failedList.size()
                + " out of "
                + (concurrentTasks.size() + 1)
                + " replicas failed to update their schema to version "
                + schemaZkVersion
                + " within "
                + maxWaitSecs
                + " seconds! Failed cores: "
                + failedList);

    } catch (InterruptedException ie) {
      log.warn(
          "Core {} was interrupted waiting for schema version {} to propagate to {} replicas for collection {}",
          localCoreNodeName,
          schemaZkVersion,
          concurrentTasks.size(),
          collection);
      Thread.currentThread().interrupt();
    } finally {
      if (!parallelExecutor.isShutdown()) parallelExecutor.shutdown();
    }

    if (log.isInfoEnabled()) {
      log.info(
          "Took {}ms for {} replicas to apply schema update version {} for collection {}",
          timer.getTime(),
          concurrentTasks.size(),
          schemaZkVersion,
          collection);
    }
  }

  protected static List<String> getActiveReplicaCoreUrls(
      ZkController zkController, String collection, String localCoreNodeName) {
    List<String> activeReplicaCoreUrls = new ArrayList<>();
    ZkStateReader zkStateReader = zkController.getZkStateReader();
    ClusterState clusterState = zkStateReader.getClusterState();
    Set<String> liveNodes = clusterState.getLiveNodes();
    final DocCollection docCollection = clusterState.getCollectionOrNull(collection);
    if (docCollection != null && docCollection.getActiveSlicesArr().length > 0) {
      final Slice[] activeSlices = docCollection.getActiveSlicesArr();
      for (Slice next : activeSlices) {
        Map<String, Replica> replicasMap = next.getReplicasMap();
        if (replicasMap != null) {
          for (Map.Entry<String, Replica> entry : replicasMap.entrySet()) {
            Replica replica = entry.getValue();
            if (!localCoreNodeName.equals(replica.getName())
                && replica.getState() == Replica.State.ACTIVE
                && liveNodes.contains(replica.getNodeName())) {
              ZkCoreNodeProps replicaCoreProps = new ZkCoreNodeProps(replica);
              activeReplicaCoreUrls.add(replicaCoreProps.getCoreUrl());
            }
          }
        }
      }
    }
    return activeReplicaCoreUrls;
  }

  private static class GetZkSchemaVersionCallable extends SolrRequest<SolrResponse>
      implements Callable<Integer> {

    private final ZkController zkController;
    private String coreUrl;
    private int expectedZkVersion;

    GetZkSchemaVersionCallable(String coreUrl, int expectedZkVersion, ZkController zkController) {
      super(METHOD.GET, "/schema/zkversion");
      this.zkController = zkController;
      this.coreUrl = coreUrl;
      this.expectedZkVersion = expectedZkVersion;
    }

    @Override
    public SolrParams getParams() {
      ModifiableSolrParams wparams = new ModifiableSolrParams();
      wparams.set("refreshIfBelowVersion", expectedZkVersion);
      return wparams;
    }

    @Override
    public Integer call() throws Exception {
      int remoteVersion = -1;
      try (HttpSolrClient solr = new HttpSolrClient.Builder(coreUrl).build()) {
        // eventually, this loop will get killed by the ExecutorService's timeout
        while (remoteVersion == -1
            || remoteVersion < expectedZkVersion && !zkController.getCoreContainer().isShutDown()) {
          try {
            HttpSolrClient.HttpUriRequestResponse mrr = solr.httpUriRequest(this);
            NamedList<Object> zkversionResp = mrr.future.get();
            if (zkversionResp != null) remoteVersion = (Integer) zkversionResp.get("zkversion");

            if (remoteVersion < expectedZkVersion) {
              // rather than waiting and re-polling, let's be proactive and tell the replica
              // to refresh its schema from ZooKeeper, if that fails, then the
              // Thread.sleep(1000); // slight delay before requesting version again
              log.error(
                  "Replica {} returned schema version {} and has not applied schema version {}",
                  coreUrl,
                  remoteVersion,
                  expectedZkVersion);
            }

          } catch (Exception e) {
            if (e instanceof InterruptedException) {
              Thread.currentThread().interrupt();
              break; // stop looping
            } else {
              log.warn("Failed to get /schema/zkversion from {} due to: ", coreUrl, e);
            }
          }
        }
      }
      return remoteVersion;
    }

    @Override
    protected SolrResponse createResponse(SolrClient client) {
      return null;
    }

    @Override
    public String getRequestType() {
      return SolrRequest.SolrRequestType.ADMIN.toString();
    }
  }

  public static class FieldExistsException extends SolrException {
    public FieldExistsException(ErrorCode code, String msg) {
      super(code, msg);
    }
  }

  public static class SchemaChangedInZkException extends SolrException {
    public SchemaChangedInZkException(ErrorCode code, String msg) {
      super(code, msg);
    }
  }

  @Override
  public ManagedIndexSchema addFields(
      Collection<SchemaField> newFields,
      Map<String, Collection<String>> copyFieldNames,
      boolean persist) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      boolean success = false;
      if (copyFieldNames == null) {
        copyFieldNames = Collections.emptyMap();
      }
      newSchema = shallowCopy(true);

      for (SchemaField newField : newFields) {
        if (null != newSchema.fields.get(newField.getName())) {
          String msg = "Field '" + newField.getName() + "' already exists.";
          throw new FieldExistsException(ErrorCode.BAD_REQUEST, msg);
        }
        newSchema.fields.put(newField.getName(), newField);

        if (null != newField.getDefaultValue()) {
          if (log.isDebugEnabled()) {
            log.debug(
                "{} contains default value: {}", newField.getName(), newField.getDefaultValue());
          }
          newSchema.fieldsWithDefaultValue.add(newField);
        }
        if (newField.isRequired()) {
          if (log.isDebugEnabled()) {
            log.debug("{} is required in this schema", newField.getName());
          }
          newSchema.requiredFields.add(newField);
        }
        Collection<String> copyFields = copyFieldNames.get(newField.getName());
        if (copyFields != null) {
          for (String copyField : copyFields) {
            newSchema.registerCopyField(newField.getName(), copyField);
          }
        }
      }

      newSchema.postReadInform();

      newSchema.refreshAnalyzers();

      if (persist) {
        // don't just create - update it if it already exists
        success = newSchema.persistManagedSchema(false);
        if (success) {
          log.debug("Added field(s): {}", newFields);
        } else {
          log.error("Failed to add field(s): {}", newFields);
          newSchema = null;
        }
      }
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  public ManagedIndexSchema deleteFields(Collection<String> names) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      newSchema = shallowCopy(true);
      for (String name : names) {
        SchemaField field = getFieldOrNull(name);
        if (null != field) {
          String message =
              "Can't delete field '"
                  + name
                  + "' because it's referred to by at least one copy field directive.";
          if (newSchema.copyFieldsMap.containsKey(name) || newSchema.isCopyFieldTarget(field)) {
            throw new SolrException(ErrorCode.BAD_REQUEST, message);
          }
          for (int i = 0; i < newSchema.dynamicCopyFields.length; ++i) {
            DynamicCopy dynamicCopy = newSchema.dynamicCopyFields[i];
            if (name.equals(dynamicCopy.getRegex())) {
              throw new SolrException(ErrorCode.BAD_REQUEST, message);
            }
          }
          newSchema.fields.remove(name);
          newSchema.fieldsWithDefaultValue.remove(field);
          newSchema.requiredFields.remove(field);
        } else {
          String msg =
              "The field '" + name + "' is not present in this schema, and so cannot be deleted.";
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
      }
      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  @SuppressWarnings({"unchecked"})
  public ManagedIndexSchema replaceField(
      String fieldName, FieldType replacementFieldType, Map<String, ?> replacementArgs) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      SchemaField oldField = fields.get(fieldName);
      if (null == oldField) {
        String msg =
            "The field '"
                + fieldName
                + "' is not present in this schema, and so cannot be replaced.";
        throw new SolrException(ErrorCode.BAD_REQUEST, msg);
      }
      newSchema = shallowCopy(true);
      // clone data structures before modifying them
      newSchema.copyFieldsMap = cloneCopyFieldsMap(copyFieldsMap);
      newSchema.copyFieldTargetCounts =
          (Map<SchemaField, Integer>)
              ((HashMap<SchemaField, Integer>) copyFieldTargetCounts).clone();
      newSchema.dynamicCopyFields = new DynamicCopy[dynamicCopyFields.length];
      System.arraycopy(
          dynamicCopyFields, 0, newSchema.dynamicCopyFields, 0, dynamicCopyFields.length);

      // Drop the old field
      newSchema.fields.remove(fieldName);
      newSchema.fieldsWithDefaultValue.remove(oldField);
      newSchema.requiredFields.remove(oldField);

      // Add the replacement field
      SchemaField replacementField =
          SchemaField.create(fieldName, replacementFieldType, replacementArgs);
      newSchema.fields.put(fieldName, replacementField);
      if (null != replacementField.getDefaultValue()) {
        if (log.isDebugEnabled()) {
          log.debug(
              "{} contains default value: {}",
              replacementField.getName(),
              replacementField.getDefaultValue());
        }
        newSchema.fieldsWithDefaultValue.add(replacementField);
      }
      if (replacementField.isRequired()) {
        if (log.isDebugEnabled()) {
          log.debug("{} is required in this schema", replacementField.getName());
        }
        newSchema.requiredFields.add(replacementField);
      }

      List<CopyField> copyFieldsToRebuild = new ArrayList<>();
      newSchema.removeCopyFieldSource(fieldName, copyFieldsToRebuild);

      newSchema.copyFieldTargetCounts.remove(oldField); // zero out target count for this field

      // Remove copy fields where the target is this field; remember them to rebuild
      for (Map.Entry<String, List<CopyField>> entry : newSchema.copyFieldsMap.entrySet()) {
        List<CopyField> perSourceCopyFields = entry.getValue();
        Iterator<CopyField> checkDestCopyFieldsIter = perSourceCopyFields.iterator();
        while (checkDestCopyFieldsIter.hasNext()) {
          CopyField checkDestCopyField = checkDestCopyFieldsIter.next();
          if (fieldName.equals(checkDestCopyField.getDestination().getName())) {
            checkDestCopyFieldsIter.remove();
            copyFieldsToRebuild.add(checkDestCopyField);
          }
        }
      }
      newSchema.rebuildCopyFields(copyFieldsToRebuild);

      // Find dynamic copy fields where the source or destination is this field; remember them to
      // rebuild
      List<DynamicCopy> dynamicCopyFieldsToRebuild = new ArrayList<>();
      List<DynamicCopy> newDynamicCopyFields = new ArrayList<>();
      for (int i = 0; i < newSchema.dynamicCopyFields.length; ++i) {
        DynamicCopy dynamicCopy = newSchema.dynamicCopyFields[i];
        SchemaField destinationPrototype = dynamicCopy.getDestination().getPrototype();
        if (fieldName.equals(dynamicCopy.getRegex())
            || fieldName.equals(destinationPrototype.getName())) {
          dynamicCopyFieldsToRebuild.add(dynamicCopy);
        } else {
          newDynamicCopyFields.add(dynamicCopy);
        }
      }
      // Rebuild affected dynamic copy fields
      if (dynamicCopyFieldsToRebuild.size() > 0) {
        newSchema.dynamicCopyFields =
            newDynamicCopyFields.toArray(new DynamicCopy[newDynamicCopyFields.size()]);
        for (DynamicCopy dynamicCopy : dynamicCopyFieldsToRebuild) {
          newSchema.registerCopyField(
              dynamicCopy.getRegex(), dynamicCopy.getDestFieldName(), dynamicCopy.getMaxChars());
        }
      }

      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  public ManagedIndexSchema addDynamicFields(
      Collection<SchemaField> newDynamicFields,
      Map<String, Collection<String>> copyFieldNames,
      boolean persist) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      boolean success = false;
      if (copyFieldNames == null) {
        copyFieldNames = Collections.emptyMap();
      }
      newSchema = shallowCopy(true);

      for (SchemaField newDynamicField : newDynamicFields) {
        List<DynamicField> dFields = new ArrayList<>(Arrays.asList(newSchema.dynamicFields));
        if (isDuplicateDynField(dFields, newDynamicField)) {
          String msg = "Dynamic field '" + newDynamicField.getName() + "' already exists.";
          throw new FieldExistsException(ErrorCode.BAD_REQUEST, msg);
        }
        dFields.add(new DynamicField(newDynamicField));
        newSchema.dynamicFields = dynamicFieldListToSortedArray(dFields);

        Collection<String> copyFields = copyFieldNames.get(newDynamicField.getName());
        if (copyFields != null) {
          for (String copyField : copyFields) {
            newSchema.registerCopyField(newDynamicField.getName(), copyField);
          }
        }
      }

      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
      if (persist) {
        // don't just create - update it if it already exists
        success = newSchema.persistManagedSchema(false);
        if (success) {
          log.debug("Added dynamic field(s): {}", newDynamicFields);
        } else {
          log.error("Failed to add dynamic field(s): {}", newDynamicFields);
        }
      }
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  public ManagedIndexSchema deleteDynamicFields(Collection<String> fieldNamePatterns) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      newSchema = shallowCopy(true);

      newSchema.dynamicCopyFields = new DynamicCopy[dynamicCopyFields.length];
      System.arraycopy(
          dynamicCopyFields, 0, newSchema.dynamicCopyFields, 0, dynamicCopyFields.length);

      List<DynamicCopy> dynamicCopyFieldsToRebuild = new ArrayList<>();
      List<DynamicCopy> newDynamicCopyFields = new ArrayList<>();

      for (String fieldNamePattern : fieldNamePatterns) {
        DynamicField dynamicField = null;
        int dfPos = 0;
        for (; dfPos < newSchema.dynamicFields.length; ++dfPos) {
          DynamicField df = newSchema.dynamicFields[dfPos];
          if (df.getRegex().equals(fieldNamePattern)) {
            dynamicField = df;
            break;
          }
        }
        if (null == dynamicField) {
          String msg =
              "The dynamic field '"
                  + fieldNamePattern
                  + "' is not present in this schema, and so cannot be deleted.";
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        for (int i = 0; i < newSchema.dynamicCopyFields.length; ++i) {
          DynamicCopy dynamicCopy = newSchema.dynamicCopyFields[i];
          DynamicField destDynamicBase = dynamicCopy.getDestDynamicBase();
          DynamicField sourceDynamicBase = dynamicCopy.getSourceDynamicBase();
          if ((null != destDynamicBase && fieldNamePattern.equals(destDynamicBase.getRegex()))
              || (null != sourceDynamicBase
                  && fieldNamePattern.equals(sourceDynamicBase.getRegex()))
              || dynamicField.matches(dynamicCopy.getRegex())
              || dynamicField.matches(dynamicCopy.getDestFieldName())) {
            dynamicCopyFieldsToRebuild.add(dynamicCopy);
            newSchema.decrementCopyFieldTargetCount(dynamicCopy.getDestination().getPrototype());
            // don't add this dynamic copy field to newDynamicCopyFields - effectively removing it
          } else {
            newDynamicCopyFields.add(dynamicCopy);
          }
        }
        if (newSchema.dynamicFields.length > 1) {
          DynamicField[] temp = new DynamicField[newSchema.dynamicFields.length - 1];
          System.arraycopy(newSchema.dynamicFields, 0, temp, 0, dfPos);
          // skip over the dynamic field to be deleted
          System.arraycopy(
              newSchema.dynamicFields,
              dfPos + 1,
              temp,
              dfPos,
              newSchema.dynamicFields.length - dfPos - 1);
          newSchema.dynamicFields = temp;
        } else {
          newSchema.dynamicFields = new DynamicField[] {};
        }
      }
      // After removing all dynamic fields, rebuild affected dynamic copy fields.
      // This may trigger an exception, if one of the deleted dynamic fields was the only matching
      // source or target.
      if (dynamicCopyFieldsToRebuild.size() > 0) {
        newSchema.dynamicCopyFields =
            newDynamicCopyFields.toArray(new DynamicCopy[newDynamicCopyFields.size()]);
        for (DynamicCopy dynamicCopy : dynamicCopyFieldsToRebuild) {
          newSchema.registerCopyField(
              dynamicCopy.getRegex(), dynamicCopy.getDestFieldName(), dynamicCopy.getMaxChars());
        }
      }

      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  @SuppressWarnings({"unchecked"})
  public ManagedIndexSchema replaceDynamicField(
      String fieldNamePattern, FieldType replacementFieldType, Map<String, ?> replacementArgs) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      DynamicField oldDynamicField = null;
      int dfPos = 0;
      for (; dfPos < dynamicFields.length; ++dfPos) {
        DynamicField dynamicField = dynamicFields[dfPos];
        if (dynamicField.getRegex().equals(fieldNamePattern)) {
          oldDynamicField = dynamicField;
          break;
        }
      }
      if (null == oldDynamicField) {
        String msg =
            "The dynamic field '"
                + fieldNamePattern
                + "' is not present in this schema, and so cannot be replaced.";
        throw new SolrException(ErrorCode.BAD_REQUEST, msg);
      }

      newSchema = shallowCopy(true);

      // clone data structures before modifying them
      newSchema.copyFieldTargetCounts =
          (Map<SchemaField, Integer>)
              ((HashMap<SchemaField, Integer>) copyFieldTargetCounts).clone();
      newSchema.dynamicCopyFields = new DynamicCopy[dynamicCopyFields.length];
      System.arraycopy(
          dynamicCopyFields, 0, newSchema.dynamicCopyFields, 0, dynamicCopyFields.length);

      // Put the replacement dynamic field in place
      SchemaField prototype =
          SchemaField.create(fieldNamePattern, replacementFieldType, replacementArgs);
      newSchema.dynamicFields[dfPos] = new DynamicField(prototype);

      // Find dynamic copy fields where this dynamic field is the source or target base; remember
      // them to rebuild
      List<DynamicCopy> dynamicCopyFieldsToRebuild = new ArrayList<>();
      List<DynamicCopy> newDynamicCopyFields = new ArrayList<>();
      for (int i = 0; i < newSchema.dynamicCopyFields.length; ++i) {
        DynamicCopy dynamicCopy = newSchema.dynamicCopyFields[i];
        DynamicField destDynamicBase = dynamicCopy.getDestDynamicBase();
        DynamicField sourceDynamicBase = dynamicCopy.getSourceDynamicBase();
        if (fieldNamePattern.equals(dynamicCopy.getRegex())
            || fieldNamePattern.equals(dynamicCopy.getDestFieldName())
            || (null != destDynamicBase && fieldNamePattern.equals(destDynamicBase.getRegex()))
            || (null != sourceDynamicBase
                && fieldNamePattern.equals(sourceDynamicBase.getRegex()))) {
          dynamicCopyFieldsToRebuild.add(dynamicCopy);
          newSchema.decrementCopyFieldTargetCount(dynamicCopy.getDestination().getPrototype());
          // don't add this dynamic copy field to newDynamicCopyFields - effectively removing it
        } else {
          newDynamicCopyFields.add(dynamicCopy);
        }
      }
      // Rebuild affected dynamic copy fields
      if (dynamicCopyFieldsToRebuild.size() > 0) {
        newSchema.dynamicCopyFields =
            newDynamicCopyFields.toArray(new DynamicCopy[newDynamicCopyFields.size()]);
        for (DynamicCopy dynamicCopy : dynamicCopyFieldsToRebuild) {
          newSchema.registerCopyField(
              dynamicCopy.getRegex(), dynamicCopy.getDestFieldName(), dynamicCopy.getMaxChars());
        }
      }

      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  public ManagedIndexSchema addCopyFields(
      Map<String, Collection<String>> copyFields, boolean persist) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      boolean success = false;
      newSchema = shallowCopy(true);
      for (Map.Entry<String, Collection<String>> entry : copyFields.entrySet()) {
        // Key is the name of the field, values are the destinations

        for (String destination : entry.getValue()) {
          newSchema.registerCopyField(entry.getKey(), destination);
        }
      }
      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
      if (persist) {
        // don't just create - update it if it already exists
        success = newSchema.persistManagedSchema(false);
        if (success) {
          if (log.isDebugEnabled()) {
            log.debug("Added copy fields for {} sources", copyFields.size());
          }
        } else {
          log.error("Failed to add copy fields for {} sources", copyFields.size());
        }
      }
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  public ManagedIndexSchema addCopyFields(
      String source, Collection<String> destinations, int maxChars) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      newSchema = shallowCopy(true);
      for (String destination : destinations) {
        newSchema.registerCopyField(source, destination, maxChars);
      }
      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  @SuppressWarnings({"unchecked"})
  public ManagedIndexSchema deleteCopyFields(Map<String, Collection<String>> copyFields) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      newSchema = shallowCopy(true);
      // clone data structures before modifying them
      newSchema.copyFieldsMap = cloneCopyFieldsMap(copyFieldsMap);
      newSchema.copyFieldTargetCounts =
          (Map<SchemaField, Integer>)
              ((HashMap<SchemaField, Integer>) copyFieldTargetCounts).clone();
      newSchema.dynamicCopyFields = new DynamicCopy[dynamicCopyFields.length];
      System.arraycopy(
          dynamicCopyFields, 0, newSchema.dynamicCopyFields, 0, dynamicCopyFields.length);

      for (Map.Entry<String, Collection<String>> entry : copyFields.entrySet()) {
        // Key is the source, values are the destinations
        for (String destination : entry.getValue()) {
          newSchema.deleteCopyField(entry.getKey(), destination);
        }
      }
      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  private void deleteCopyField(String source, String dest) {
    // Assumption: a copy field directive will exist only if the source & destination (dynamic)
    // fields exist
    SchemaField destSchemaField = fields.get(dest);
    SchemaField sourceSchemaField = fields.get(source);

    final String invalidGlobMessage =
        "is an invalid glob: either it contains more than one asterisk,"
            + " or the asterisk occurs neither at the start nor at the end.";
    if (source.contains("*") && !isValidFieldGlob(source)) {
      String msg = "copyField source '" + source + "' " + invalidGlobMessage;
      throw new SolrException(ErrorCode.BAD_REQUEST, msg);
    }
    if (dest.contains("*") && !isValidFieldGlob(dest)) {
      String msg = "copyField dest '" + dest + "' " + invalidGlobMessage;
      throw new SolrException(ErrorCode.BAD_REQUEST, msg);
    }

    boolean found = false;

    if (null == destSchemaField || null == sourceSchemaField) { // Must be dynamic copy field
      for (int i = 0; i < dynamicCopyFields.length; ++i) {
        DynamicCopy dynamicCopy = dynamicCopyFields[i];
        if (source.equals(dynamicCopy.getRegex()) && dest.equals(dynamicCopy.getDestFieldName())) {
          found = true;
          SchemaField destinationPrototype = dynamicCopy.getDestination().getPrototype();
          if (copyFieldTargetCounts.containsKey(destinationPrototype)) {
            decrementCopyFieldTargetCount(destinationPrototype);
          }
          if (dynamicCopyFields.length > 1) {
            DynamicCopy[] temp = new DynamicCopy[dynamicCopyFields.length - 1];
            System.arraycopy(dynamicCopyFields, 0, temp, 0, i);
            // skip over the dynamic copy field to be deleted
            System.arraycopy(dynamicCopyFields, i + 1, temp, i, dynamicCopyFields.length - i - 1);
            dynamicCopyFields = temp;
          } else {
            dynamicCopyFields = new DynamicCopy[] {};
          }
          break;
        }
      }
    }

    if (!found) {
      // non-dynamic copy field directive.
      // Here, source field could either exists in schema or match a dynamic rule
      List<CopyField> copyFieldList = copyFieldsMap.get(source);
      if (copyFieldList != null) {
        for (Iterator<CopyField> iter = copyFieldList.iterator(); iter.hasNext(); ) {
          CopyField copyField = iter.next();
          if (dest.equals(copyField.getDestination().getName())) {
            found = true;
            decrementCopyFieldTargetCount(copyField.getDestination());
            iter.remove();
            if (copyFieldList.isEmpty()) {
              copyFieldsMap.remove(source);
            }
            break;
          }
        }
      }
    }
    if (!found) {
      throw new SolrException(
          ErrorCode.BAD_REQUEST,
          "Copy field directive not found: '" + source + "' -> '" + dest + "'");
    }
  }

  /**
   * Removes all copy fields with the given source field name, decrements the count for the copy
   * field target, and adds the removed copy fields to removedCopyFields.
   */
  private void removeCopyFieldSource(String sourceFieldName, List<CopyField> removedCopyFields) {
    List<CopyField> sourceCopyFields = copyFieldsMap.remove(sourceFieldName);
    if (null != sourceCopyFields) {
      for (CopyField sourceCopyField : sourceCopyFields) {
        decrementCopyFieldTargetCount(sourceCopyField.getDestination());
        removedCopyFields.add(sourceCopyField);
      }
    }
  }

  /**
   * Registers new copy fields with the source, destination and maxChars taken from each of the
   * oldCopyFields.
   *
   * <p>Assumption: the fields in oldCopyFields still exist in the schema.
   */
  private void rebuildCopyFields(List<CopyField> oldCopyFields) {
    if (oldCopyFields.size() > 0) {
      for (CopyField copyField : oldCopyFields) {
        // source or destination either could be explicit field which matches dynamic rule
        SchemaField source = getFieldOrNull(copyField.getSource().getName());
        SchemaField destination = getFieldOrNull(copyField.getDestination().getName());
        registerExplicitSrcAndDestFields(
            copyField.getSource().getName(), copyField.getMaxChars(), destination, source);
      }
    }
  }

  /** Decrements the count for the given destination field in copyFieldTargetCounts. */
  private void decrementCopyFieldTargetCount(SchemaField dest) {
    Integer count = copyFieldTargetCounts.get(dest);
    assert count != null;
    if (count <= 1) {
      copyFieldTargetCounts.remove(dest);
    } else {
      copyFieldTargetCounts.put(dest, count - 1);
    }
  }

  public ManagedIndexSchema addFieldTypes(List<FieldType> fieldTypeList, boolean persist) {
    if (!isMutable) {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }

    ManagedIndexSchema newSchema = shallowCopy(true);

    // we shallow copied fieldTypes, but since we're changing them, we need to do a true
    // deep copy before adding the new field types
    @SuppressWarnings({"unchecked"})
    HashMap<String, FieldType> clone =
        (HashMap<String, FieldType>) ((HashMap<String, FieldType>) newSchema.fieldTypes).clone();
    newSchema.fieldTypes = clone;

    // do a first pass to validate the field types don't exist already
    for (FieldType fieldType : fieldTypeList) {
      String typeName = fieldType.getTypeName();
      if (newSchema.getFieldTypeByName(typeName) != null) {
        throw new FieldExistsException(
            ErrorCode.BAD_REQUEST, "Field type '" + typeName + "' already exists!");
      }

      newSchema.fieldTypes.put(typeName, fieldType);
    }

    newSchema.postReadInform();

    newSchema.refreshAnalyzers();

    if (persist) {
      boolean success = newSchema.persistManagedSchema(false);
      if (success) {
        if (log.isDebugEnabled()) {
          StringBuilder fieldTypeNames = new StringBuilder();
          for (int i = 0; i < fieldTypeList.size(); i++) {
            if (i > 0) fieldTypeNames.append(", ");
            fieldTypeNames.append(fieldTypeList.get(i).typeName);
          }
          log.debug("Added field types: {}", fieldTypeNames);
        }
      } else {
        // this is unlikely to happen as most errors are handled as exceptions in the persist code
        log.error("Failed to add field types: {}", fieldTypeList);
        throw new SolrException(
            ErrorCode.SERVER_ERROR,
            "Failed to persist updated schema due to underlying storage issue; check log for more details!");
      }
    }

    return newSchema;
  }

  @Override
  public ManagedIndexSchema deleteFieldTypes(Collection<String> names) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      for (String name : names) {
        if (!fieldTypes.containsKey(name)) {
          String msg =
              "The field type '"
                  + name
                  + "' is not present in this schema, and so cannot be deleted.";
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        for (SchemaField field : fields.values()) {
          if (field.getType().getTypeName().equals(name)) {
            throw new SolrException(
                ErrorCode.BAD_REQUEST,
                "Can't delete '"
                    + name
                    + "' because it's the field type of field '"
                    + field.getName()
                    + "'.");
          }
        }
        for (DynamicField dynamicField : dynamicFields) {
          if (dynamicField.getPrototype().getType().getTypeName().equals(name)) {
            throw new SolrException(
                ErrorCode.BAD_REQUEST,
                "Can't delete '"
                    + name
                    + "' because it's the field type of dynamic field '"
                    + dynamicField.getRegex()
                    + "'.");
          }
        }
      }
      newSchema = shallowCopy(true);
      for (String name : names) {
        newSchema.fieldTypes.remove(name);
      }
      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  private Map<String, List<CopyField>> cloneCopyFieldsMap(Map<String, List<CopyField>> original) {
    Map<String, List<CopyField>> clone = new HashMap<>(original.size());
    Iterator<Map.Entry<String, List<CopyField>>> iterator = original.entrySet().iterator();
    while (iterator.hasNext()) {
      Map.Entry<String, List<CopyField>> entry = iterator.next();
      clone.put(entry.getKey(), new ArrayList<>(entry.getValue()));
    }
    return clone;
  }

  @Override
  @SuppressWarnings({"unchecked"})
  public ManagedIndexSchema replaceFieldType(
      String typeName, String replacementClassName, Map<String, Object> replacementArgs) {
    ManagedIndexSchema newSchema;
    if (isMutable) {
      if (!fieldTypes.containsKey(typeName)) {
        String msg =
            "The field type '"
                + typeName
                + "' is not present in this schema, and so cannot be replaced.";
        throw new SolrException(ErrorCode.BAD_REQUEST, msg);
      }
      newSchema = shallowCopy(true);
      // clone data structures before modifying them
      newSchema.fieldTypes =
          (Map<String, FieldType>) ((HashMap<String, FieldType>) fieldTypes).clone();
      newSchema.copyFieldsMap = cloneCopyFieldsMap(copyFieldsMap);
      newSchema.copyFieldTargetCounts =
          (Map<SchemaField, Integer>)
              ((HashMap<SchemaField, Integer>) copyFieldTargetCounts).clone();
      newSchema.dynamicCopyFields = new DynamicCopy[dynamicCopyFields.length];
      System.arraycopy(
          dynamicCopyFields, 0, newSchema.dynamicCopyFields, 0, dynamicCopyFields.length);
      newSchema.dynamicFields = new DynamicField[dynamicFields.length];
      System.arraycopy(dynamicFields, 0, newSchema.dynamicFields, 0, dynamicFields.length);

      newSchema.fieldTypes.remove(typeName);
      FieldType replacementFieldType =
          newSchema.newFieldType(typeName, replacementClassName, replacementArgs);
      newSchema.fieldTypes.put(typeName, replacementFieldType);

      // Rebuild fields of the type being replaced
      List<CopyField> copyFieldsToRebuild = new ArrayList<>();
      List<SchemaField> replacementFields = new ArrayList<>();
      Iterator<Map.Entry<String, SchemaField>> fieldsIter = newSchema.fields.entrySet().iterator();
      while (fieldsIter.hasNext()) {
        Map.Entry<String, SchemaField> entry = fieldsIter.next();
        SchemaField oldField = entry.getValue();
        if (oldField.getType().getTypeName().equals(typeName)) {
          String fieldName = oldField.getName();

          // Drop the old field
          fieldsIter.remove();
          newSchema.fieldsWithDefaultValue.remove(oldField);
          newSchema.requiredFields.remove(oldField);

          // Add the replacement field
          SchemaField replacementField =
              SchemaField.create(fieldName, replacementFieldType, oldField.getArgs());
          // Save the new field to be added after iteration is finished
          replacementFields.add(replacementField);
          if (null != replacementField.getDefaultValue()) {
            if (log.isDebugEnabled()) {
              log.debug(
                  "{} contains default value: {}",
                  replacementField.getName(),
                  replacementField.getDefaultValue());
            }
            newSchema.fieldsWithDefaultValue.add(replacementField);
          }
          if (replacementField.isRequired()) {
            if (log.isDebugEnabled()) {
              log.debug("{} is required in this schema", replacementField.getName());
            }
            newSchema.requiredFields.add(replacementField);
          }
          newSchema.removeCopyFieldSource(fieldName, copyFieldsToRebuild);
        }
      }
      for (SchemaField replacementField : replacementFields) {
        newSchema.fields.put(replacementField.getName(), replacementField);
      }
      // Remove copy fields where the target is of the type being replaced; remember them to rebuild
      Iterator<Map.Entry<String, List<CopyField>>> copyFieldsMapIter =
          newSchema.copyFieldsMap.entrySet().iterator();
      while (copyFieldsMapIter.hasNext()) {
        Map.Entry<String, List<CopyField>> entry = copyFieldsMapIter.next();
        List<CopyField> perSourceCopyFields = entry.getValue();
        Iterator<CopyField> checkDestCopyFieldsIter = perSourceCopyFields.iterator();
        while (checkDestCopyFieldsIter.hasNext()) {
          CopyField checkDestCopyField = checkDestCopyFieldsIter.next();
          SchemaField destination = checkDestCopyField.getDestination();
          if (typeName.equals(destination.getType().getTypeName())) {
            checkDestCopyFieldsIter.remove();
            copyFieldsToRebuild.add(checkDestCopyField);
            newSchema.copyFieldTargetCounts.remove(destination); // zero out target count
          }
        }
        if (perSourceCopyFields.isEmpty()) {
          copyFieldsMapIter.remove();
        }
      }
      // Rebuild dynamic fields of the type being replaced
      for (int i = 0; i < newSchema.dynamicFields.length; ++i) {
        SchemaField prototype = newSchema.dynamicFields[i].getPrototype();
        if (typeName.equals(prototype.getType().getTypeName())) {
          newSchema.dynamicFields[i] =
              new DynamicField(
                  SchemaField.create(
                      prototype.getName(), replacementFieldType, prototype.getArgs()));
        }
      }
      // Find dynamic copy fields where the destination field's type is being replaced
      // or the source dynamic base's type is being replaced; remember them to rebuild
      List<DynamicCopy> dynamicCopyFieldsToRebuild = new ArrayList<>();
      List<DynamicCopy> newDynamicCopyFields = new ArrayList<>();
      for (int i = 0; i < newSchema.dynamicCopyFields.length; ++i) {
        DynamicCopy dynamicCopy = newSchema.dynamicCopyFields[i];
        DynamicField sourceDynamicBase = dynamicCopy.getSourceDynamicBase();
        SchemaField destinationPrototype = dynamicCopy.getDestination().getPrototype();
        if (typeName.equals(destinationPrototype.getType().getTypeName())
            || (null != sourceDynamicBase
                && typeName.equals(sourceDynamicBase.getPrototype().getType().getTypeName()))) {
          dynamicCopyFieldsToRebuild.add(dynamicCopy);
          if (newSchema.copyFieldTargetCounts.containsKey(destinationPrototype)) {
            newSchema.decrementCopyFieldTargetCount(destinationPrototype);
          }
          // don't add this dynamic copy field to newDynamicCopyFields - effectively removing it
        } else {
          newDynamicCopyFields.add(dynamicCopy);
        }
      }
      // Rebuild affected dynamic copy fields
      if (dynamicCopyFieldsToRebuild.size() > 0) {
        newSchema.dynamicCopyFields =
            newDynamicCopyFields.toArray(new DynamicCopy[newDynamicCopyFields.size()]);
        for (DynamicCopy dynamicCopy : dynamicCopyFieldsToRebuild) {
          newSchema.registerCopyField(
              dynamicCopy.getRegex(), dynamicCopy.getDestFieldName(), dynamicCopy.getMaxChars());
        }
      }
      newSchema.rebuildCopyFields(copyFieldsToRebuild);

      newSchema.postReadInform();
      newSchema.refreshAnalyzers();
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return newSchema;
  }

  @Override
  protected void postReadInform() {
    super.postReadInform();
    for (FieldType fieldType : fieldTypes.values()) {
      informResourceLoaderAwareObjectsForFieldType(fieldType);
    }
  }

  /** Informs analyzers used by a fieldType. */
  protected void informResourceLoaderAwareObjectsForFieldType(FieldType fieldType) {
    // must inform any sub-components used in the
    // tokenizer chain if they are ResourceLoaderAware
    if (!fieldType.supportsAnalyzers()) return;

    Analyzer indexAnalyzer = fieldType.getIndexAnalyzer();
    if (indexAnalyzer != null && indexAnalyzer instanceof TokenizerChain)
      informResourceLoaderAwareObjectsInChain((TokenizerChain) indexAnalyzer);

    Analyzer queryAnalyzer = fieldType.getQueryAnalyzer();
    // ref comparison is correct here (vs. equals) as they may be the same
    // object in which case, we don't need to inform twice ... however, it's
    // actually safe to call inform multiple times on an object anyway
    if (queryAnalyzer != null
        && queryAnalyzer != indexAnalyzer
        && queryAnalyzer instanceof TokenizerChain)
      informResourceLoaderAwareObjectsInChain((TokenizerChain) queryAnalyzer);

    // if fieldType is a TextField, it might have a multi-term analyzer
    if (fieldType instanceof TextField) {
      TextField textFieldType = (TextField) fieldType;
      Analyzer multiTermAnalyzer = textFieldType.getMultiTermAnalyzer();
      if (multiTermAnalyzer != null
          && multiTermAnalyzer != indexAnalyzer
          && multiTermAnalyzer != queryAnalyzer
          && multiTermAnalyzer instanceof TokenizerChain)
        informResourceLoaderAwareObjectsInChain((TokenizerChain) multiTermAnalyzer);
    }
  }

  @Override
  public SchemaField newField(String fieldName, String fieldType, Map<String, ?> options) {
    SchemaField sf;
    if (isMutable) {
      try {
        if (-1 != fieldName.indexOf('*')) {
          String msg = "Can't add dynamic field '" + fieldName + "'.";
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        SchemaField existingFieldWithTheSameName = fields.get(fieldName);
        if (null != existingFieldWithTheSameName) {
          String msg = "Field '" + fieldName + "' already exists.";
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        FieldType type = getFieldTypeByName(fieldType);
        if (null == type) {
          String msg = "Field '" + fieldName + "': Field type '" + fieldType + "' not found.";
          log.error(msg);
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        sf = SchemaField.create(fieldName, type, options);
      } catch (SolrException e) {
        throw e;
      } catch (Exception e) {
        throw new SolrException(ErrorCode.BAD_REQUEST, e);
      }
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return sf;
  }

  public int getSchemaZkVersion() {
    return schemaZkVersion;
  }

  @Override
  public SchemaField newDynamicField(
      String fieldNamePattern, String fieldType, Map<String, ?> options) {
    SchemaField sf;
    if (isMutable) {
      try {
        FieldType type = getFieldTypeByName(fieldType);
        if (null == type) {
          String msg =
              "Dynamic field '" + fieldNamePattern + "': Field type '" + fieldType + "' not found.";
          log.error(msg);
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
        sf = SchemaField.create(fieldNamePattern, type, options);
        if (!isValidDynamicField(Arrays.asList(dynamicFields), sf)) {
          String msg = "Invalid dynamic field '" + fieldNamePattern + "'";
          log.error(msg);
          throw new SolrException(ErrorCode.BAD_REQUEST, msg);
        }
      } catch (SolrException e) {
        throw e;
      } catch (Exception e) {
        throw new SolrException(ErrorCode.BAD_REQUEST, e);
      }
    } else {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }
    return sf;
  }

  @Override
  public FieldType newFieldType(String typeName, String className, Map<String, ?> options) {
    if (!isMutable) {
      String msg = "This ManagedIndexSchema is not mutable.";
      log.error(msg);
      throw new SolrException(ErrorCode.SERVER_ERROR, msg);
    }

    if (getFieldTypeByName(typeName) != null) {
      String msg = "Field type '" + typeName + "' already exists.";
      log.error(msg);
      throw new SolrException(ErrorCode.BAD_REQUEST, msg);
    }

    // build the new FieldType using the existing FieldTypePluginLoader framework
    // which expects XML, so we use a JSON to XML adapter to transform the JSON object
    // provided in the request into the XML format supported by the plugin loader
    Map<String, FieldType> newFieldTypes = new HashMap<>();
    List<SchemaAware> schemaAwareList = new ArrayList<>();
    FieldTypePluginLoader typeLoader =
        new FieldTypePluginLoader(this, newFieldTypes, schemaAwareList);
    typeLoader.loadSingle(solrClassLoader, new DOMConfigNode(FieldTypeXmlAdapter.toNode(options)));
    FieldType ft = newFieldTypes.get(typeName);
    if (!schemaAwareList.isEmpty()) schemaAware.addAll(schemaAwareList);

    return ft;
  }

  /**
   * After creating a new FieldType, it may contain components that implement the
   * ResourceLoaderAware interface, which need to be informed after they are loaded (as they depend
   * on this callback to complete initialization work)
   */
  protected void informResourceLoaderAwareObjectsInChain(TokenizerChain chain) {
    CharFilterFactory[] charFilters = chain.getCharFilterFactories();
    for (CharFilterFactory next : charFilters) {
      if (next instanceof ResourceLoaderAware) {
        try {
          informAware(loader, (ResourceLoaderAware) next);
        } catch (IOException e) {
          throw new SolrException(ErrorCode.SERVER_ERROR, e);
        }
      }
    }

    TokenizerFactory tokenizerFactory = chain.getTokenizerFactory();
    if (tokenizerFactory instanceof ResourceLoaderAware) {
      try {
        informAware(loader, (ResourceLoaderAware) tokenizerFactory);
      } catch (IOException e) {
        throw new SolrException(ErrorCode.SERVER_ERROR, e);
      }
    }

    TokenFilterFactory[] filters = chain.getTokenFilterFactories();
    for (TokenFilterFactory next : filters) {
      if (next instanceof ResourceLoaderAware) {
        try {
          informAware(loader, (ResourceLoaderAware) next);
        } catch (IOException e) {
          throw new SolrException(ErrorCode.SERVER_ERROR, e);
        }
      }
    }
  }

  private ManagedIndexSchema(
      Version luceneVersion,
      SolrResourceLoader loader,
      boolean isMutable,
      String managedSchemaResourceName,
      int schemaZkVersion,
      Object schemaUpdateLock,
      Properties substitutableProps) {
    super(luceneVersion, loader, substitutableProps);
    this.isMutable = isMutable;
    this.managedSchemaResourceName = managedSchemaResourceName;
    this.schemaZkVersion = schemaZkVersion;
    this.schemaUpdateLock = schemaUpdateLock;
  }

  /**
   * Makes a shallow copy of this schema.
   *
   * <p>Not copied: analyzers
   *
   * @param includeFieldDataStructures if true, fields, fieldsWithDefaultValue, and requiredFields
   *     are copied; otherwise, they are not.
   * @return A shallow copy of this schema
   */
  ManagedIndexSchema shallowCopy(boolean includeFieldDataStructures) {
    ManagedIndexSchema newSchema =
        new ManagedIndexSchema(
            luceneVersion,
            loader,
            isMutable,
            managedSchemaResourceName,
            schemaZkVersion,
            getSchemaUpdateLock(),
            substitutableProperties);

    newSchema.name = name;
    newSchema.version = version;
    newSchema.similarity = similarity;
    newSchema.similarityFactory = similarityFactory;
    newSchema.isExplicitSimilarity = isExplicitSimilarity;
    newSchema.uniqueKeyField = uniqueKeyField;
    newSchema.uniqueKeyFieldName = uniqueKeyFieldName;
    newSchema.uniqueKeyFieldType = uniqueKeyFieldType;

    // After the schema is persisted, resourceName is the same as managedSchemaResourceName
    newSchema.resourceName = managedSchemaResourceName;

    if (includeFieldDataStructures) {
      // These need new collections, since addFields() can add members to them
      newSchema.fields.putAll(fields);
      newSchema.fieldsWithDefaultValue.addAll(fieldsWithDefaultValue);
      newSchema.requiredFields.addAll(requiredFields);
    }

    // These don't need new collections - addFields() won't add members to them
    newSchema.fieldTypes = fieldTypes;
    newSchema.dynamicFields = dynamicFields;
    newSchema.dynamicCopyFields = dynamicCopyFields;
    newSchema.copyFieldsMap = copyFieldsMap;
    newSchema.copyFieldTargetCounts = copyFieldTargetCounts;
    newSchema.schemaAware = schemaAware;

    return newSchema;
  }

  @Override
  public Object getSchemaUpdateLock() {
    return schemaUpdateLock;
  }
}
