H2DB.java

/*
 * Copyright 2013 greg.
 *
 * Licensed 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 nz.co.gregs.dbvolution.databases;

import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import nz.co.gregs.dbvolution.databases.metadata.DBDatabaseMetaData;
import nz.co.gregs.dbvolution.databases.metadata.H2DBDatabaseMetaData;
import nz.co.gregs.dbvolution.databases.metadata.Options;
import nz.co.gregs.dbvolution.databases.settingsbuilders.H2SettingsBuilder;
import nz.co.gregs.dbvolution.databases.settingsbuilders.AbstractH2SettingsBuilder;
import nz.co.gregs.dbvolution.databases.settingsbuilders.H2FileSettingsBuilder;
import nz.co.gregs.dbvolution.exceptions.ExceptionDuringDatabaseFeatureSetup;
import nz.co.gregs.dbvolution.internal.h2.*;
import nz.co.gregs.dbvolution.internal.query.StatementDetails;
import nz.co.gregs.regexi.Regex;
import org.h2.jdbc.JdbcException;

/**
 * Stores all the required functionality to use an H2 database.
 *
 * @author Gregory Graham
 */
public class H2DB extends DBDatabaseImplementation {

	private static final long serialVersionUID = 1l;
	public static final String DRIVER_NAME = "org.h2.Driver";
	private final static Map<String, DBVFeature> FEATURE_MAP = new HashMap<>();
	private static boolean dataTypesNotProcessed = true;

	static {
		try {
			Class.forName(DRIVER_NAME);
		} catch (ClassNotFoundException ex) {
			Logger.getLogger(H2DB.class.getName()).log(Level.SEVERE, null, ex);
		}
		for (DBVFeature function : DateRepeatFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DBVFeature function : Point2DFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DBVFeature function : LineSegment2DFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DBVFeature function : Line2DFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DBVFeature function : Polygon2DFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DBVFeature function : MultiPoint2DFunctions.values()) {
			FEATURE_MAP.put(function.alias(), function);
		}
		for (DataTypes datatype : DataTypes.values()) {
			FEATURE_MAP.put(datatype.alias(), datatype);
		}
	}

	/**
	 * Creates a DBDatabase for a H2 database in the file supplied.
	 *
	 * <p>
	 * This creates a database connection to a local H2 database stored in the
	 * file provided.
	 *
	 * <p>
	 * If the file does not exist the database will be created in the file.
	 *
	 * Database exceptions may be thrown
	 *
	 * @param file file
	 * @param username username
	 * @param password password
	 * @throws java.io.IOException java.io.IOException
	 * @throws java.sql.SQLException java.sql.SQLException
	 */
	public H2DB(File file, String username, String password) throws IOException, SQLException {
		this(new H2FileSettingsBuilder()
				.setDatabaseName(file.getCanonicalFile().toString())
				.setUsername(username)
				.setPassword(password)
				.toSettings()
		);
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 * <p>
	 * Database exceptions may be thrown
	 *
	 * @param dataSource dataSource
	 * @throws java.sql.SQLException database errors
	 */
	public H2DB(DataSource dataSource) throws SQLException {
		super(
				new H2SettingsBuilder().setDataSource(dataSource)
		);
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 * <p>
	 * Database exceptions may be thrown
	 *
	 * @param dcs dataSource
	 * @throws java.sql.SQLException database errors
	 */
	public H2DB(DatabaseConnectionSettings dcs) throws SQLException {
		super(new H2SettingsBuilder().fromSettings(dcs));
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 * <p>
	 * Database exceptions may be thrown
	 *
	 * @param settings dataSource
	 * @throws java.sql.SQLException database errors
	 */
	protected H2DB(AbstractH2SettingsBuilder<?, ?> settings) throws SQLException {
		super(settings);
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 *
	 *
	 *
	 * 1 Database exceptions may be thrown
	 *
	 * @param jdbcURL jdbcURL
	 * @param username username
	 * @param password password
	 * @throws java.sql.SQLException database errors
	 */
	public H2DB(String jdbcURL, String username, String password) throws SQLException {
		this(new H2SettingsBuilder()
				.fromJDBCURL(jdbcURL)
				.setUsername(username)
				.setPassword(password)
				.toSettings()
		);
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 *
	 *
	 *
	 * 1 Database exceptions may be thrown
	 *
	 * @param databaseFilename the name and path of the database file
	 * @param username username
	 * @param password password
	 * @param dummy unused
	 * @throws java.sql.SQLException database errors
	 */
	public H2DB(String databaseFilename, String username, String password, boolean dummy) throws SQLException {
		this(new H2FileSettingsBuilder()
				.setFilename(databaseFilename)
				.setUsername(username)
				.setPassword(password)
				.toSettings()
		);
	}

	/**
	 * Creates a DBDatabase for a H2 database.
	 *
	 * <p>
	 * Database exceptions may be thrown</p>
	 *
	 * @param settings the settings that specify everything necessary to connect
	 * to the H2 database
	 * @throws java.sql.SQLException database errors
	 */
	public H2DB(H2SettingsBuilder settings) throws SQLException {
		this(settings.toSettings());
	}

	@Override
	public synchronized void addDatabaseSpecificFeatures(final Statement stmt) throws ExceptionDuringDatabaseFeatureSetup {
		DataTypes.addAll(stmt);
		if (dataTypesNotProcessed) {
			for (DataTypes datatype : DataTypes.values()) {
				FEATURE_MAP.put(datatype.alias(), datatype);
			}
			dataTypesNotProcessed = false;
		}
	}

	/**
	 * Clones the DBDatabase
	 *
	 * @return a clone of the database.
	 * @throws java.lang.CloneNotSupportedException
	 * java.lang.CloneNotSupportedException
	 *
	 */
	@Override
	public H2DB clone() throws CloneNotSupportedException {
		return (H2DB) super.clone();
	}

	private final static Regex BROKEN_CONNECTION_PATTERN = Regex.startingAnywhere().literal("Connection is broken: \"session closed\"").toRegex();
	private final static Regex ALREADY_CLOSED_PATTERN = Regex.startingAnywhere().literal("The object is already closed").toRegex();
	private final static Regex DROPPING_NONEXISTENT_TABLE_PATTERN = Regex.startingAnywhere().literal("Table \"").beginNamedCapture("table").noneOfTheseCharacters("\"").oneOrMore().endNamedCapture().literal("\" not found; SQL statement:").anyCharacter().optionalMany().literal("DROP TABLE ").namedBackReference("table").toRegex();
	private final static Regex TABLE_NOT_FOUND_WHILE_CHECKING_EXISTENCE_PATTERN = Regex.startingAnywhere().literal("Table \"").noneOfTheseCharacters("\"").oneOrMore().literal("\" not found").anyCharacterIncludingLineEnd().optionalMany().literal("SQL statement:").anyCharacterIncludingLineEnd().optionalMany().literal("SELECT COUNT(").star().literal(")").toRegex();
	private final static Regex CREATING_EXISTING_TABLE_PATTERN = Regex.startingAnywhere().literal("Table \"").anythingButThis("\"").oneOrMore().literal("\" already exists; SQL statement:").toRegex();

	@Override
	public ResponseToException addFeatureToFixException(Exception exp, QueryIntention intent, StatementDetails details) throws Exception {
		boolean handledException = false;
		if ((exp instanceof JdbcException)) {
			String message = exp.getMessage();
			if (message != null) {
				if (BROKEN_CONNECTION_PATTERN.matchesWithinString(message)
						|| ALREADY_CLOSED_PATTERN.matchesWithinString(message)) {
					return ResponseToException.REPLACECONNECTION;
				} else if (DROPPING_NONEXISTENT_TABLE_PATTERN.matchesWithinString(message)) {
					return ResponseToException.SKIPQUERY;
				} else if (QueryIntention.CHECK_TABLE_EXISTS.equals(intent) && TABLE_NOT_FOUND_WHILE_CHECKING_EXISTENCE_PATTERN.matchesWithinString(message)) {
					return ResponseToException.SKIPQUERY;
				} else if (CREATING_EXISTING_TABLE_PATTERN.matchesWithinString(message)) {
					return ResponseToException.SKIPQUERY;
				} else {
					try (DBStatement statement = getConnection().createDBStatement()) {
						if ((message.startsWith("Function \"DBV_") && message.contains("\" not found"))
								|| (message.startsWith("Method \"DBV_") && message.contains("\" not found"))) {
							String[] split = message.split("[\" ]+");
							String functionName = split[1];
							DBVFeature functions = FEATURE_MAP.get(functionName);
							if (functions != null) {
								functions.add(statement.getInternalStatement());
								handledException = true;
							}
						} else if (message.startsWith("Unknown data type: \"DBV_")) {
							String[] split = message.split("\"");
							String functionName = split[1];
							DBVFeature datatype = FEATURE_MAP.get(functionName);
							if (datatype != null) {
								datatype.add(statement.getInternalStatement());
								handledException = true;
							}
						} else if (message.matches(": +method \"DBV_[A-Z_0-9]+")) {
							String[] split = message.split("method \"");
							split = split[1].split("\\(");
							String functionName = split[0];

							DBVFeature functions = FEATURE_MAP.get(functionName);
							if (functions != null) {
								functions.add(statement.getInternalStatement());
								handledException = true;
							}
						} else {
							for (Map.Entry<String, DBVFeature> entrySet : FEATURE_MAP.entrySet()) {
								String key = entrySet.getKey();
								DBVFeature value = entrySet.getValue();
								if (message.contains(key)) {
									value.add(statement.getInternalStatement());
									handledException = true;
								}
							}
						}
					}
				}
			}
		}
		if (!handledException) {
			throw exp;
		} else {
			return ResponseToException.REQUERY;
		}
	}

	@Override
	public boolean isMemoryDatabase() {
		return getJdbcURL().contains(":mem:");
	}

	@Override
	public Integer getDefaultPort() {
		return 9123;
	}

	private final static H2SettingsBuilder URL_PROCESSOR = new H2SettingsBuilder();

	@Override
	public AbstractH2SettingsBuilder<?, ?> getURLInterpreter() {
		return URL_PROCESSOR;
	}

	@Override
	public boolean supportsGeometryTypesFullyInSchema() {
		return true;
	}

	@Override
	public DBDatabaseMetaData getDBDatabaseMetaData(Options options) throws SQLException {
		return new H2DBDatabaseMetaData(options);
	}
}