view src/main/java/nl/cwi/monetdb/jdbc/MonetDriver.java.in @ 93:eeb71f7d36bf embedded

Fixed a bug on the JDBC MAPI connection from the old code! Fixed the connection properties for an JDBC Embedded connection. To start a JDBC Embedded connection, the user must start the embedded database beforehand with the method MonetDBEmbeddedDatabase.StartDatabase().
author Pedro Ferreira <pedro.ferreira@monetdbsolutions.com>
date Fri, 06 Jan 2017 12:36:33 +0000 (2017-01-06)
parents 6f74e01c57da
children 64530632dc2a
line wrap: on
line source
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 1997 - July 2008 CWI, August 2008 - 2017 MonetDB B.V.
 */

package nl.cwi.monetdb.jdbc;

import nl.cwi.monetdb.mcl.connection.MCLException;
import nl.cwi.monetdb.mcl.connection.mapi.MapiConnection;
import nl.cwi.monetdb.mcl.connection.mapi.MapiLanguage;
import nl.cwi.monetdb.mcl.protocol.ProtocolException;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Types;
import java.util.*;
import java.util.Map.Entry;
import java.util.logging.Logger;

/**
 * A Driver suitable for the MonetDB database.
 *
 * This driver will be used by the DriverManager to determine if an URL
 * is to be handled by this driver, and if it does, then this driver
 * will supply a Connection suitable for MonetDB.
 *
 * This class has no explicit constructor, the default constructor
 * generated by the Java compiler will be sufficient since nothing has
 * to be set in order to use this driver.
 *
 * This Driver supports MonetDB database URLs. MonetDB URLs are defined
 * as:
 * <tt>jdbc:monetdb://&lt;host&gt;[:&lt;port&gt;]/&lt;database&gt;</tt>
 * where [:&lt;port&gt;] denotes that a port is optional. If not
 * given the default (@JDBC_DEF_PORT@) will be used.
 *
 * @author Fabian Groffen
 * @version @JDBC_MAJOR@.@JDBC_MINOR@ (@JDBC_VER_SUFFIX@)
 */
final public class MonetDriver implements Driver {
	// the url kind will be jdbc:monetdb://<host>[:<port>]/<database>
	// Chapter 9.2.1 from Sun JDBC 3.0 specification
	/** The prefix of a MonetDB url */
	private static final String MONETURL = "jdbc:monetdb://";
	/** Major version of this driver */
	private static final int DRIVERMAJOR = @JDBC_MAJOR@;
	/** Minor version of this driver */
	private static final int DRIVERMINOR = @JDBC_MINOR@;

	/** Version suffix string */
	private static final String DRIVERVERSIONSUFFIX =
			"@JDBC_VER_SUFFIX@ based on MCL v@MCL_MAJOR@.@MCL_MINOR@";
	// We're not fully compliant, but what we support is compliant
	/** Whether this driver is JDBC compliant or not */
	private static final boolean MONETJDBCCOMPLIANT = false;

	/** MonetDB default port to connect to */
	private static final String PORT = "@JDBC_DEF_PORT@";

    private static Class EmbeddedConnectionClass = null;

	// initialize this class: register it at the DriverManager
	// Chapter 9.2 from Sun JDBC 3.0 specification
	static {
		try {
			DriverManager.registerDriver(new MonetDriver());
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	//== methods of interface Driver

	/**
	 * Retrieves whether the driver thinks that it can open a connection to the
	 * given URL. Typically drivers will return true if they understand the
	 * subprotocol specified in the URL and false if they do not.
	 *
	 * @param url the URL of the database
	 * @return true if this driver understands the given URL; false otherwise
	 */
	public boolean acceptsURL(String url) {
		return url != null && url.startsWith(MONETURL);
	}

	/**
	 * Retrieves the driver's major version number. Initially this should be 1.
	 *
	 * @return this driver's major version number
	 */
	public int getMajorVersion() {
		return DRIVERMAJOR;
	}

	/**
	 * Gets the driver's minor version number. Initially this should be 0.
	 *
	 * @return this driver's minor version number
	 */
	public int getMinorVersion() {
		return DRIVERMINOR;
	}

	/**
	 * Gets information about the possible properties for this driver.
	 *
	 * The getPropertyInfo method is intended to allow a generic GUI tool to
	 * discover what properties it should prompt a human for in order to get
	 * enough information to connect to a database. Note that depending on the
	 * values the human has supplied so far, additional values may become
	 * necessary, so it may be necessary to iterate though several calls to the
	 * getPropertyInfo method.
	 *
	 * @param url the URL of the database to which to connect
	 * @param info a proposed list of tag/value pairs that will be sent on connect open
	 * @return an array of DriverPropertyInfo objects describing possible properties. This array may be an empty array
	 * if no properties are required.
	 */
	public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
		if (!acceptsURL(url))
			return null;

		List<DriverPropertyInfo> props = new ArrayList<>();

		DriverPropertyInfo prop = new DriverPropertyInfo("user", info.getProperty("user"));
		prop.required = true;
		prop.description = "The user name to use when authenticating on the database server";
		props.add(prop);

		prop = new DriverPropertyInfo("password", info.getProperty("password"));
		prop.required = true;
		prop.description = "The password to use when authenticating on the database server";
		props.add(prop);

		prop = new DriverPropertyInfo("hash", "");
		prop.required = false;
		prop.description = "Force the use of the given hash algorithm during challenge response (one of SHA1, MD5, plain)";
		props.add(prop);

		prop = new DriverPropertyInfo("host", "localhost");
		prop.required = false;
		prop.description = "The MonetDB server hostname (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("port", PORT);
		prop.required = false;
		prop.description = "The port to connect to the MonetDB server (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("so_timeout", "0");
		prop.required = false;
		prop.description = "Defines the maximum time to wait in milliseconds on a blocking read socket call (MAPI connection only)"; // this corresponds to the Connection.setNetworkTimeout() method introduced in JDBC 4.1
		props.add(prop);

		prop = new DriverPropertyInfo("database", "");
		prop.required = false;
		prop.description = "The database name to connect (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("follow_redirects", "true");
		prop.required = false;
		prop.description = "Whether redirects issued by the server should be followed (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("treat_blob_as_binary", "false");
		prop.required = false;
		prop.description = "Whether BLOBs on the server should be treated as LONGVARBINARY types, thus mapped to byte[] (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("treat_clob_as_longvarchar", "false");
		prop.required = false;
		prop.description = "Whether CLOBs on the server should be treated as LONGVARCHAR types, thus mapped to String (MAPI connection only)";
		props.add(prop);

		prop = new DriverPropertyInfo("language", "sql");
		prop.required = false;
		prop.description = "What language to use for MonetDB conversations (experts only)";
		props.add(prop);

		prop = new DriverPropertyInfo("embedded", "false");
		prop.required = false;
		prop.description = "Whether or not to use an embedded MonetDB connection";
		props.add(prop);

		DriverPropertyInfo[] dpi = new DriverPropertyInfo[props.size()];
		return props.toArray(dpi);
	}

	/**
	 * Reports whether this driver is a genuine JDBC Compliant&tm; driver. A
	 * driver may only report true here if it passes the JDBC compliance tests;
	 * otherwise it is required to return false.
	 *
	 * JDBC compliance requires full support for the JDBC API and full support
	 * for SQL 92 Entry Level. It is expected that JDBC compliant drivers will
	 * be available for all the major commercial databases.
	 *
	 * This method is not intended to encourage the development of non-JDBC
	 * compliant drivers, but is a recognition of the fact that some vendors are
	 * interested in using the JDBC API and framework for lightweight databases
	 * that do not support full database functionality, or for special databases
	 * such as document information retrieval where a SQL implementation may not
	 * be feasible.
	 *
	 * @return true if this driver is JDBC Compliant; false otherwise
	 */
	public boolean jdbcCompliant() {
		return MONETJDBCCOMPLIANT;
	}

	//== end methods of interface driver

	/** A static Map containing the mapping between MonetDB types and Java SQL types */
	/* use SELECT sqlname, * FROM sys.types order by 1, id; to view all MonetDB types */
	/* see http://docs.oracle.com/javase/7/docs/api/java/sql/Types.html to view all supported java SQL types */
	private static Map<String, Integer> typeMap = new HashMap<>();
	static {
		// fill the typeMap once
		// typeMap.put("any", Types.???);
		typeMap.put("bigint", Types.BIGINT);
		typeMap.put("blob", Types.BLOB);
		typeMap.put("boolean", Types.BOOLEAN);
		typeMap.put("char", Types.CHAR);
		typeMap.put("clob", Types.CLOB);
		typeMap.put("date", Types.DATE);
		typeMap.put("decimal", Types.DECIMAL);
		typeMap.put("double", Types.DOUBLE);
		typeMap.put("geometry", Types.OTHER);
		typeMap.put("geometrya", Types.OTHER);
		typeMap.put("hugeint", Types.NUMERIC); //but we will convert to java.math.BigInteger
		typeMap.put("inet", Types.OTHER);
		typeMap.put("int", Types.INTEGER);
		typeMap.put("json", Types.OTHER);
		// typeMap.put("mbr", Types.???);
		typeMap.put("month_interval", Types.INTEGER);
		// typeMap.put("oid", Types.BIGINT);
		// typeMap.put("ptr", Types.???);
		typeMap.put("real", Types.REAL);
		typeMap.put("sec_interval", Types.BIGINT);
		typeMap.put("smallint", Types.SMALLINT);
		// typeMap.put("table", Types.???);
		typeMap.put("time", Types.TIME);
		typeMap.put("timestamp", Types.TIMESTAMP);
		typeMap.put("timestamptz", Types.TIMESTAMP_WITH_TIMEZONE);
		typeMap.put("timetz", Types.TIME_WITH_TIMEZONE);
		typeMap.put("tinyint", Types.TINYINT); //but we will convert to java.lang.Byte
		typeMap.put("url", Types.OTHER);
		typeMap.put("uuid", Types.OTHER);
		typeMap.put("varchar", Types.VARCHAR);
		typeMap.put("wrd", Types.BIGINT);
	}

	/**
	 * Returns the java.sql.Types equivalent of the given MonetDB type.
	 *
	 * @param type the type as used by MonetDB
	 * @return the matching java.sql.Types constant or java.sql.Types.OTHER if nothing matched on the given string
	 */
	public static int getJavaType(String type) {
		// match the currentColumns type on a java.sql.Types constant
		Integer tp = typeMap.get(type);
		if (tp != null) {
			return tp;
		} else {
			// this should not be able to happen do not assert, since maybe future versions introduce new types
			return Types.OTHER;
		}
	}

	private static String TypeMapppingSQL = null; // cache to optimise getSQLTypeMap()

	/**
	 * Returns a String usable in an SQL statement to map the server types
	 * to values of java.sql.Types using the global static type map.
	 * The returned string will be a SQL CASE x statement where the x is
	 * replaced with the given currentColumns name (or expression) string.
	 *
	 * @param column a String representing the value that should be evaluated in the SQL CASE statement
	 * @return a SQL CASE statement
	 */
	static String getSQLTypeMap(String column) {
		if (TypeMapppingSQL == null) {
			// first time, compose TypeMappping SQL string
			StringBuilder val = new StringBuilder((typeMap.size() * (7 + 7 + 7 + 4)) + 14);
			for (Entry<String, Integer> entry : typeMap.entrySet()) {
				val.append(" WHEN '").append(entry.getKey()).append("' THEN ").append(entry.getValue().toString());
			}
			val.append(" ELSE ").append(Types.OTHER).append(" END");
			// as the typeMap is static, cache this SQL part for all next calls
			TypeMapppingSQL = val.toString();
		}
		return "CASE " + column + TypeMapppingSQL;
	}

	/**
	 * Returns a touched up identifying version string of this driver.
	 *
	 * @return the version string
	 */
	public static String getDriverVersion() {
		return "" + DRIVERMAJOR + "." + DRIVERMINOR + " (" + DRIVERVERSIONSUFFIX + ")";
	}

	public static int getDriverMajorVersion() {
		return DRIVERMAJOR;
	}

	public static int getDriverMinorVersion() {
		return DRIVERMINOR;
	}

	/**
	 * Return the parent Logger of all the Loggers used by this data
	 * source.  This should be the Logger farthest from the root Logger
	 * that is still an ancestor of all of the Loggers used by this data
	 * source.  Configuring this Logger will affect all of the log
	 * messages generated by the data source. In the worst case, this
	 * may be the root Logger.
	 *
	 * @return the parent Logger for this data source
	 * @throws SQLFeatureNotSupportedException if the data source does not use java.util.logging
	 */
	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
		throw new SQLFeatureNotSupportedException("java.util.logging not in use", "0A000");
	}

	/**
	 * Attempts to make a database connection to the given URL. The driver
	 * should return "null" if it realizes it is the wrong kind of driver to
	 * connect to the given URL. This will be common, as when the JDBC driver
	 * manager is asked to connect to a given URL it passes the URL to each
	 * loaded driver in turn.
	 *
	 * The driver should throw an SQLException if it is the right driver to
	 * connect to the given URL but has trouble connecting to the database.
	 *
	 * The java.util.Properties argument can be used to pass arbitrary string
	 * tag/value pairs as connection arguments. Normally at least "user" and
	 * "password" properties should be included in the Properties object.
	 *
	 * @param url the URL of the database to which to connect
	 * @param info a list of arbitrary string tag/value pairs as connection
	 *        arguments. Normally at least a "user" and "password" property
	 *        should be included
	 * @return a Connection object that represents a connection to the URL
	 * @throws SQLException if a database access error occurs
	 */
	public Connection connect(String url, Properties info) throws SQLException {
		int tmp;
		Properties props = new Properties();
		props.put("port", PORT);
		props.putAll(info);
		info = props;

		// url should be of style jdbc:monetdb://<host>/<database>
		if (!acceptsURL(url))
			throw new SQLException("Invalid URL: it does not start with: " + MONETURL, "08M26");

		// remove leading "jdbc:" so the rest is a valid hierarchical URI
		URI uri;
		try {
			uri = new URI(url.substring(5));
		} catch (URISyntaxException e) {
			throw new SQLException(e.toString(), "08M26");
		}

		String uri_host = uri.getHost();
		if (uri_host == null)
			throw new SQLException("Invalid URL: no hostname given or unparsable in '" + url + "'", "08M26");
		info.put("host", uri_host);

		int uri_port = uri.getPort();
		if (uri_port > 0)
			info.put("port", "" + uri_port);

		// check the database
		String uri_path = uri.getPath();
		if (uri_path != null && uri_path.length() != 0) {
			uri_path = uri_path.substring(1);
			if (!uri_path.trim().isEmpty())
				info.put("database", uri_path);
		}

		String uri_query = uri.getQuery();
		if (uri_query != null) {
			// handle additional arguments
			String args[] = uri_query.split("&");
			for (String arg : args) {
				tmp = arg.indexOf('=');
				if (tmp > 0)
					info.put(arg.substring(0, tmp), arg.substring(tmp + 1));
			}
		}

		// finally return the Connection as requested
		return CreateMonetDBJDBCConnection(info);
	}

    @SuppressWarnings("unchecked")
	private static MonetConnection CreateMonetDBJDBCConnection(Properties props) throws SQLException,
			IllegalArgumentException {
		MonetConnection res;

		boolean isEmbedded = Boolean.parseBoolean(props.getProperty("embedded", "false"));
		String language = props.getProperty("language", "sql");
		String username = props.getProperty("user");
		String password = props.getProperty("password");
		String hash = props.getProperty("hash");
		int sockTimeout = 0;

		if(isEmbedded) { //instantiate the connection
			try {
			    if(EmbeddedConnectionClass == null) {
			        EmbeddedConnectionClass = Class.forName("nl.cwi.monetdb.embedded.jdbc.EmbeddedConnection");
			    }
				if(EmbeddedConnectionClass == null) { //if it's still null then there is a problem
					throw new SQLException("EmbeddedConnection Class not found!");
				}
                res = (MonetConnection) EmbeddedConnectionClass
                    .getDeclaredConstructor(Properties.class, String.class, String.class)
                    .newInstance(props, hash, language);
			} catch (InvocationTargetException | InstantiationException | IllegalAccessException |
			         NoSuchMethodException | ClassNotFoundException e) {
				throw new SQLException(e);
			}
		} else {
			String hostname = props.getProperty("host");
			if (hostname == null || hostname.trim().isEmpty())
				throw new IllegalArgumentException("hostname should not be null or empty");
			if (username == null || username.trim().isEmpty())
				throw new IllegalArgumentException("user should not be null or empty");
			if (password == null || password.trim().isEmpty())
				throw new IllegalArgumentException("password should not be null or empty");
            String database = props.getProperty("database");
            if (database == null || database.trim().isEmpty())
                throw new IllegalArgumentException("database should not be null or empty");

            boolean blobIsBinary = Boolean.valueOf(props.getProperty("treat_blob_as_binary", "false"));
            boolean clobIsLongChar = Boolean.valueOf(props.getProperty("treat_clob_as_longvarchar", "false"));

			boolean negative1 = false, failedparse1 = false;
			int port = 0;
			try {
				port = Integer.parseInt(props.getProperty("port"));
			} catch (NumberFormatException e) {
				failedparse1 = true;
				props.setProperty("port", PORT);
			}
			if (port <= 0) {
				negative1 = true;
				port = Integer.parseInt(PORT);
				props.setProperty("port", PORT);
			}

			String timeout = props.getProperty("so_timeout", "0");
			boolean negative2 = false, failedparse2 = false;
			try {
				sockTimeout = Integer.parseInt(timeout);
			} catch (NumberFormatException e) {
				sockTimeout = 0;
				failedparse2 = true;
				props.setProperty("so_timeout", "0");
			}
			if (sockTimeout < 0) {
				negative2 = true;
				sockTimeout = 0;
				props.setProperty("so_timeout", "0");
			}
			try {
				res = new MapiConnection(props, hash, language, blobIsBinary, clobIsLongChar, hostname, port, database);
			} catch (IOException e) {
				throw new SQLException(e);
			}
			if(failedparse1) {
				res.addWarning("Unable to parse port number from: " + port, "M1M05");
			}
			if(negative1) {
				res.addWarning("Negative port not allowed. Value ignored", "M1M05");
			}
			if(failedparse2) {
				res.addWarning("Unable to parse socket timeout number from: " + timeout, "M1M05");
			}
			if(negative2) {
				res.addWarning("Negative socket timeout not allowed. Value ignored", "M1M05");
			}
			res.setSoTimeout(sockTimeout);
		}

		try { //atempt to connect and authenticate the user
			List<String> warnings = res.connect(username, password);
			if(warnings != null) {
				for (String warning : warnings) {
					res.addWarning(warning, "01M02");
				}
			}
			// apply NetworkTimeout value from legacy (pre 4.1) driver so_timeout calls
			if(!isEmbedded) {
				res.setSoTimeout(sockTimeout);
			}
		} catch (IOException e) {
			if(!isEmbedded) {
				MapiConnection con = (MapiConnection) res;
				throw new SQLException("Unable to connect (" + con.getHostname() + ":"
						+ con.getPort() + "): " + e.getMessage(), "08006");
			} else {
                throw new SQLException("Unable to connect: " + e.getMessage(), "08006");
			}
		} catch (ProtocolException e) {
			throw new SQLException(e.getMessage(), "08001");
		} catch (MCLException e) {
			String[] connex = e.getMessage().split("\n");
			SQLException sqle = new SQLException(connex[0], "08001", e);
			for (int i = 1; i < connex.length; i++) {
				sqle.setNextException(new SQLException(connex[1], "08001"));
			}
			throw sqle;
		}

		if (!isEmbedded && res.getLanguage() == MapiLanguage.LANG_SQL) { //set the timezone only in the MAPI connection
			// enable auto commit
			res.setAutoCommit(true);
			// set our time zone on the server
			Calendar cal = Calendar.getInstance();
			int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET);
			offset /= (60 * 1000); // milliseconds to minutes
			String tz = offset < 0 ? "-" : "+";
			tz += (Math.abs(offset) / 60 < 10 ? "0" : "") + (Math.abs(offset) / 60) + ":";
			offset -= (offset / 60) * 60;
			tz += (offset < 10 ? "0" : "") + offset;

			res.sendIndependentCommand("SET TIME ZONE INTERVAL '" + tz + "' HOUR TO MINUTE");
		}

		return res;
	}
}