ProxyingMetadataHandlerProvider.java

/*
 * 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);
  }

}