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

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implementation of JDBC driver that does not register itself.
 *
 * <p>You can easily create a "vanity driver" that recognizes its own
 * URL prefix as a sub-class of this class. Per the JDBC specification it
 * must register itself when the class is loaded.</p>
 *
 * <p>Derived classes must implement {@link #createDriverVersion()} and
 * {@link #getConnectStringPrefix()}, and may override
 * {@link #createFactory()}.</p>
 *
 * <p>The provider must implement:</p>
 * <ul>
 *   <li>{@link Meta#prepare(Meta.ConnectionHandle, String, long)}
 *   <li>{@link Meta#createIterable(org.apache.calcite.avatica.Meta.StatementHandle, org.apache.calcite.avatica.QueryState, org.apache.calcite.avatica.Meta.Signature, java.util.List, Meta.Frame)}
 * </ul>
 */
public abstract class UnregisteredDriver implements java.sql.Driver {
  final DriverVersion version;
  protected final AvaticaFactory factory;
  public final Handler handler;

  protected UnregisteredDriver() {
    this.factory = createFactory();
    this.version = createDriverVersion();
    this.handler = createHandler();
  }

  /**
   * Creates a factory for JDBC objects (connection, statement).
   * Called from the driver constructor.
   *
   * <p>The default implementation calls {@link JdbcVersion#current},
   * then {@link #getFactoryClassName} with that version,
   * then passes that class name to {@link #instantiateFactory(String)}.
   * This approach is recommended it does not include in the code references
   * to classes that may not be instantiable in all JDK versions.
   * But drivers are free to do it their own way.</p>
   *
   * @return JDBC object factory
   */
  protected AvaticaFactory createFactory() {
    return instantiateFactory(getFactoryClassName(JdbcVersion.current()));
  }

  /** Creates a Handler. */
  protected Handler createHandler() {
    return new HandlerImpl();
  }

  /**
   * Returns the name of a class to be factory for JDBC objects
   * (connection, statement) appropriate for the current JDBC version.
   */
  protected String getFactoryClassName(JdbcVersion jdbcVersion) {
    switch (jdbcVersion) {
    case JDBC_30:
    case JDBC_40:
      throw new IllegalArgumentException("JDBC version not supported: "
          + jdbcVersion);
    case JDBC_41:
    default:
      return "org.apache.calcite.avatica.AvaticaJdbc41Factory";
    }
  }

  /**
   * Creates an object describing the name and version of this driver.
   * Called from the driver constructor.
   */
  protected abstract DriverVersion createDriverVersion();

  /**
   * Returns the connection properties supported by this driver.
   */
  protected Collection<ConnectionProperty> getConnectionProperties() {
    return Arrays.<ConnectionProperty>asList(
        BuiltInConnectionProperty.values());
  }

  /** Helper method for creating factories. */
  protected static AvaticaFactory instantiateFactory(String factoryClassName) {
    try {
      final Class<?> clazz = Class.forName(factoryClassName);
      return (AvaticaFactory) clazz.getConstructor().newInstance();
    } catch (Throwable e) {
      // It is not usually good to catch Throwable. But class loading can fail
      // with serious errors such as java.lang.NoClassDefFoundError
      throw handle("Error loading factory " + factoryClassName, e);
    }
  }

  private static RuntimeException handle(String msg, Throwable e) {
    Logger.getLogger("").log(Level.SEVERE, msg, e);
    throw new RuntimeException(msg, e);
  }

  public Connection connect(String url, Properties info) throws SQLException {
    if (!acceptsURL(url)) {
      return null;
    }
    final String prefix = getConnectStringPrefix();
    assert url.startsWith(prefix);
    final String urlSuffix = url.substring(prefix.length());
    final Properties info2 = ConnectStringParser.parse(urlSuffix, info);
    final AvaticaConnection connection =
        factory.newConnection(this, factory, url, info2);
    handler.onConnectionInit(connection);
    return connection;
  }

  public boolean acceptsURL(String url) throws SQLException {
    return url.startsWith(getConnectStringPrefix());
  }

  /** Returns the prefix of the connect string that this driver will recognize
   * as its own. For example, "jdbc:calcite:". */
  protected abstract String getConnectStringPrefix();

  public DriverPropertyInfo[] getPropertyInfo(
      String url, Properties info) throws SQLException {
    List<DriverPropertyInfo> list = new ArrayList<DriverPropertyInfo>();

    // First, add the contents of info
    for (Map.Entry<Object, Object> entry : info.entrySet()) {
      list.add(
          new DriverPropertyInfo(
              (String) entry.getKey(),
              (String) entry.getValue()));
    }
    // Next, add property definitions not mentioned in info
    for (ConnectionProperty p : getConnectionProperties()) {
      if (info.containsKey(p.name())) {
        continue;
      }
      list.add(new DriverPropertyInfo(p.name(), null));
    }
    return list.toArray(new DriverPropertyInfo[list.size()]);
  }

  // JDBC 4.1 support (JDK 1.7 and higher)
  public Logger getParentLogger() {
    return Logger.getLogger("");
  }

  /**
   * Returns the driver version object. Not in the JDBC API.
   *
   * @return Driver version
   */
  public DriverVersion getDriverVersion() {
    return version;
  }

  public final int getMajorVersion() {
    return version.majorVersion;
  }

  public final int getMinorVersion() {
    return version.minorVersion;
  }

  public boolean jdbcCompliant() {
    return version.jdbcCompliant;
  }

  /**
   * Registers this driver with the driver manager.
   */
  protected void register() {
    try {
      DriverManager.registerDriver(this);
    } catch (SQLException e) {
      System.out.println(
          "Error occurred while registering JDBC driver "
          + this + ": " + e.toString());
    }
  }

  /** Creates a service handler that will give connections from this Driver
   * their behavior. */
  public abstract Meta createMeta(AvaticaConnection connection);

  /** JDBC version. */
  protected enum JdbcVersion {
    /** Unknown JDBC version. */
    JDBC_UNKNOWN,
    /** JDBC version 3.0. Generally associated with JDK 1.5. */
    JDBC_30,
    /** JDBC version 4.0. Generally associated with JDK 1.6. */
    JDBC_40,
    /** JDBC version 4.1. Generally associated with JDK 1.7. */
    JDBC_41;

    /** Deduces the current JDBC version. */
    public static JdbcVersion current() {
      try {
        // If java.sql.PseudoColumnUsage is present, we are running JDBC
        // 4.1 or later.
        Class.forName("java.sql.PseudoColumnUsage");
        return JDBC_41;
      } catch (ClassNotFoundException e) {
        // java.sql.PseudoColumnUsage is not present. This means we are
        // running JDBC 4.0 or earlier.
        try {
          Class.forName("java.sql.Wrapper");
          return JDBC_40;
        } catch (ClassNotFoundException e2) {
          // java.sql.Wrapper is not present. This means we are
          // running JDBC 3.0 or earlier (probably JDK 1.5).
          return JDBC_30;
        }
      }
    }
  }
}

// End UnregisteredDriver.java