content: /*
   * 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
   *
   *     https://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.avro.compiler.idl;
  
  import org.apache.avro.Protocol;
  import org.apache.avro.Schema;
  import org.apache.avro.compiler.schema.Schemas;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.IdentityHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.function.Function;
  
  /**
   * Utility class to resolve schemas that are unavailable at the time they are
   * referenced in the IDL.
   */
  final class SchemaResolver {
  
    private SchemaResolver() {
    }
  
    private static final String UR_SCHEMA_ATTR = "org.apache.avro.compiler.idl.unresolved.name";
  
    private static final String UR_SCHEMA_NAME = "UnresolvedSchema";
  
    private static final String UR_SCHEMA_NS = "org.apache.avro.compiler";
  
    private static final AtomicInteger COUNTER = new AtomicInteger();
  
    /**
     * Create a schema to represent a "unresolved" schema. (used to represent a
     * schema where the definition is not known at the time) This concept might be
     * generalizable...
     *
     * @param name
     * @return
     */
    static Schema unresolvedSchema(final String name) {
      Schema schema = Schema.createRecord(UR_SCHEMA_NAME + '_' + COUNTER.getAndIncrement(), "unresolved schema",
          UR_SCHEMA_NS, false, Collections.EMPTY_LIST);
      schema.addProp(UR_SCHEMA_ATTR, name);
      return schema;
    }
  
    /**
     * Is this a unresolved schema.
     *
     * @param schema
     * @return
     */
    static boolean isUnresolvedSchema(final Schema schema) {
      return (schema.getType() == Schema.Type.RECORD && schema.getProp(UR_SCHEMA_ATTR) != null && schema.getName() != null
          && schema.getName().startsWith(UR_SCHEMA_NAME) && UR_SCHEMA_NS.equals(schema.getNamespace()));
    }
  
    /**
     * get the unresolved schema name.
     *
     * @param schema
     * @return
     */
    static String getUnresolvedSchemaName(final Schema schema) {
      if (!isUnresolvedSchema(schema)) {
        throw new IllegalArgumentException("Not a unresolved schema: " + schema);
      }
      return schema.getProp(UR_SCHEMA_ATTR);
    }
  
    /**
     * Is this a unresolved schema.
     *
     * @param schema
     * @return
     */
    static boolean isFullyResolvedSchema(final Schema schema) {
      if (isUnresolvedSchema(schema)) {
        return false;
      } else {
        return Schemas.visit(schema, new IsResolvedSchemaVisitor());
      }
    }
  
    /**
     * Will clone the provided protocol while resolving all unreferenced schemas
     *
     * @param protocol a protocol with possibly unresolved schema references
     * @return a protocol without unresolved schema references
     */
    static Protocol resolve(final Protocol protocol) {
      Protocol result = new Protocol(protocol.getName(), protocol.getDoc(), protocol.getNamespace());
      final Collection<Schema> types = protocol.getUnresolvedTypes();
      // replace unresolved schemas.
      List<Schema> newSchemas = new ArrayList<>(types.size());
      IdentityHashMap<Schema, Schema> replacements = new IdentityHashMap<>();
      for (Schema schema : types) {
        newSchemas.add(Schemas.visit(schema, new ResolvingVisitor(schema, replacements, new SymbolTable(protocol))));
      }
      result.setTypes(newSchemas); // replace types with resolved ones
  
      // Resolve all schemas referenced by protocol Messages.
      for (Map.Entry<String, Protocol.Message> entry : protocol.getMessages().entrySet()) {
        Protocol.Message value = entry.getValue();
        Protocol.Message nvalue;
        if (value.isOneWay()) {
          Schema replacement = resolve(replacements, value.getRequest(), protocol);
          nvalue = result.createMessage(value.getName(), value.getDoc(), value, replacement);
        } else {
          Schema request = resolve(replacements, value.getRequest(), protocol);
          Schema response = resolve(replacements, value.getResponse(), protocol);
          Schema errors = resolve(replacements, value.getErrors(), protocol);
          nvalue = result.createMessage(value.getName(), value.getDoc(), value, request, response, errors);
        }
        result.getMessages().put(entry.getKey(), nvalue);
      }
      Schemas.copyProperties(protocol, result);
      return result;
    }
  
    private static Schema resolve(final IdentityHashMap<Schema, Schema> replacements, final Schema request,
        final Protocol protocol) {
      Schema replacement = replacements.get(request);
      if (replacement == null) {
        replacement = Schemas.visit(request, new ResolvingVisitor(request, replacements, new SymbolTable(protocol)));
      }
      return replacement;
    }
  
    private static class SymbolTable implements Function<String, Schema> {
  
      private final Protocol symbolTable;
  
      public SymbolTable(Protocol symbolTable) {
        this.symbolTable = symbolTable;
      }
  
      @Override
      public Schema apply(final String f) {
        return symbolTable.getType(f);
      }
    }
  
  }
  