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
   *
   * 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.calcite.rel.metadata;
  
  import org.apache.calcite.rel.RelNode;
  
  import java.lang.reflect.Field;
  import java.lang.reflect.InvocationHandler;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.lang.reflect.ParameterizedType;
  import java.lang.reflect.Proxy;
  import java.lang.reflect.Type;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.stream.Collectors;
  
  import static java.util.Objects.requireNonNull;
  
  /**
   * A MetadataHandlerProvider built on a RelMetadataProvider.
   *
   * <p>Uses proxies to call the underlying metadata provider.
   */
  public class ProxyingMetadataHandlerProvider implements MetadataHandlerProvider {
  
    private final RelMetadataProvider provider;
  
    /**
     * Create a proxying handler provider.
     *
     * @param provider The provider this will operate against.
     */
    public ProxyingMetadataHandlerProvider(RelMetadataProvider provider) {
      this.provider = provider;
    }
  
    @SuppressWarnings("deprecation")
    @Override public <MH extends MetadataHandler<?>> MH handler(Class<MH> handlerClass) {
  
      Type[] types = handlerClass.getGenericInterfaces();
      if (types.length != 1 || !(types[0] instanceof ParameterizedType)) {
        throw new UnsupportedOperationException("Unexpected failure. " + handlerClass);
      }
  
      ParameterizedType pType = (ParameterizedType) types[0];
      if (pType.getRawType() != MetadataHandler.class) {
        throw new UnsupportedOperationException("Unexpected failure. " + handlerClass);
      }
      Class<?> metadataType = (Class<?>) pType.getActualTypeArguments()[0];
      final Field field;
      final MetadataDef<?> def;
      try {
        field = metadataType.getField("DEF");
        def =
            requireNonNull((MetadataDef<?>) field.get(null),
                () -> "Unexpected failure. " + handlerClass);
      } catch (NoSuchFieldException | IllegalAccessException e) {
        throw new RuntimeException(e);
      }
  
      List<Method> methods = def.methods;
      Map<String, Method> methodMap = methods.stream()
          .collect(Collectors.toMap(Method::getName, f -> f));
      InvocationHandler handler = (proxy, method, args) -> {
        Method metadataMethod =
            requireNonNull(methodMap.get(method.getName()),
                () -> "Not supported: " + method);
        RelNode rel = requireNonNull((RelNode) args[0], "rel must be non null");
        RelMetadataQuery mq =
            requireNonNull((RelMetadataQuery) args[1], "mq must be non null");
  
        // using deprecated RelMetadataProvider method here as the non-deprecated methods completely
        // sidestep the purpose of RelMetadataProvider reflection-based functionality.
        @SuppressWarnings({"unchecked", "rawtypes"})
        UnboundMetadata metadata =
            provider.apply(rel.getClass(),
                (Class<? extends Metadata>) metadataType);
  
        if (metadata == null) {
          Method handlerMethod =
              Arrays.stream(handlerClass.getMethods())
                  .filter(m -> m.getName().equals(metadataMethod.getName()))
                  .findFirst()
                  .orElseThrow(()
                      -> new IllegalArgumentException("Unable to find method."));
          throw new IllegalArgumentException(
              String.format(Locale.ROOT, "No handler for method [%s] applied to "
                  + "argument of type [%s]; we recommend you create a catch-all "
                  + "(RelNode) handler", handlerMethod, rel.getClass()));
        }
        Metadata bound =
            requireNonNull(metadata, "expected defined metadata")
                .bind(rel, mq);
  
        Object[] abbreviatedArgs = new Object[args.length - 2];
        System.arraycopy(args, 2, abbreviatedArgs, 0, abbreviatedArgs.length);
        try {
          return metadataMethod.invoke(bound, abbreviatedArgs);
        } catch (InvocationTargetException ex) {
          if (ex.getCause() instanceof CyclicMetadataException) {
            throw (CyclicMetadataException) ex.getCause();
          }
  
          throw new RuntimeException(ex.getCause());
        }
      };
  
      return (MH) Proxy.newProxyInstance(
          handlerClass.getClassLoader(),
          new Class[]{handlerClass},
          handler);
    }
  
  }
  