blob: 84d417bce77685f85ea341d8eaa9aa9736540d5a [file] [log] [blame]
/*
* 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.jackrabbit.oak.plugins.document.rdb;
import static com.google.common.collect.Iterables.cycle;
import static com.google.common.collect.Iterables.limit;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.SQLTransientException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import org.apache.jackrabbit.oak.plugins.document.DocumentStoreException;
import org.apache.jackrabbit.oak.plugins.document.util.UTF8Encoder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
/**
* Convenience methods dealing with JDBC specifics.
*/
public class RDBJDBCTools {
private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(RDBJDBCTools.class);
protected static String jdbctype(String jdbcurl) {
if (jdbcurl == null) {
return null;
} else {
String t = jdbcurl.toLowerCase(Locale.ENGLISH);
if (!t.startsWith("jdbc:")) {
return null;
} else {
t = t.substring("jbdc:".length());
int p = t.indexOf(":");
if (p <= 0) {
return t;
} else {
return t.substring(0, p);
}
}
}
}
protected static String driverForDBType(String type) {
if ("h2".equals(type)) {
return "org.h2.Driver";
} else if ("derby".equals(type)) {
return "org.apache.derby.jdbc.EmbeddedDriver";
} else if ("postgresql".equals(type)) {
return "org.postgresql.Driver";
} else if ("db2".equals(type)) {
return "com.ibm.db2.jcc.DB2Driver";
} else if ("mysql".equals(type)) {
return "com.mysql.jdbc.Driver";
} else if ("oracle".equals(type)) {
return "oracle.jdbc.OracleDriver";
} else if ("sqlserver".equals(type)) {
return "com.microsoft.sqlserver.jdbc.SQLServerDriver";
} else {
return "";
}
}
private static @NotNull String checkLegalTableName(@NotNull String tableName) throws IllegalArgumentException {
for (int i = 0; i < tableName.length(); i++) {
char c = tableName.charAt(i);
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_'))) {
throw new IllegalArgumentException("Invalid character '" + c + "' in table name '" + tableName + "'");
}
}
return tableName;
}
/**
* Creates a table name based on an optional prefix and a base name.
*
* @throws IllegalArgumentException
* upon illegal characters in name
*/
protected static @NotNull String createTableName(@NotNull String prefix, @NotNull String basename)
throws IllegalArgumentException {
String p = checkLegalTableName(prefix);
String b = checkLegalTableName(basename);
if (p.length() != 0 && !p.endsWith("_")) {
p += "_";
}
return p + b;
}
/**
* Return string representation of transaction isolation level.
*/
protected static @NotNull String isolationLevelToString(int isolationLevel) {
String name;
switch (isolationLevel) {
case Connection.TRANSACTION_NONE:
name = "TRANSACTION_NONE";
break;
case Connection.TRANSACTION_READ_COMMITTED:
name = "TRANSACTION_READ_COMMITTED";
break;
case Connection.TRANSACTION_READ_UNCOMMITTED:
name = "TRANSACTION_READ_UNCOMMITTED";
break;
case Connection.TRANSACTION_REPEATABLE_READ:
name = "TRANSACTION_REPEATABLE_READ";
break;
case Connection.TRANSACTION_SERIALIZABLE:
name = "TRANSACTION_SERIALIZABLE";
break;
default:
name = "unknown";
break;
}
return String.format("%s (%d)", name, isolationLevel);
}
private static String dumpColumnMeta(String columnName, int type, String typeName, int precision) {
boolean skipPrecision = precision == 0 || (type == Types.SMALLINT && precision == 5)
|| (type == Types.BIGINT && precision == 19);
return skipPrecision ? String.format("%s %s", columnName, typeName)
: String.format("%s %s(%d)", columnName, typeName, precision);
}
/**
* Return approximated string representation of table DDL.
*/
protected static String dumpResultSetMeta(ResultSetMetaData met) {
try {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%s.%s: ", met.getSchemaName(1).trim(), met.getTableName(1).trim()));
Map<String, Integer> types = new TreeMap<String, Integer>();
for (int i = 1; i <= met.getColumnCount(); i++) {
if (i > 1) {
sb.append(", ");
}
sb.append(
dumpColumnMeta(met.getColumnName(i), met.getColumnType(i), met.getColumnTypeName(i), met.getPrecision(i)));
types.put(met.getColumnTypeName(i), met.getColumnType(i));
}
sb.append(" /* " + types.toString() + " */");
return sb.toString();
} catch (SQLException ex) {
return "Column metadata unavailable: " + ex.getMessage();
}
}
/**
* Return a string containing additional messages from chained exceptions.
*/
protected static @NotNull String getAdditionalMessages(SQLException ex) {
List<String> messages = new ArrayList<String>();
String message = ex.getMessage();
SQLException next = ex.getNextException();
while (next != null) {
String m = next.getMessage();
if (!message.equals(m)) {
messages.add(m);
}
next = next.getNextException();
}
return messages.isEmpty() ? "" : messages.toString();
}
/**
* Check whether the exception matches one of the given states.
*/
protected static boolean matchesSQLState(SQLException ex, String... statePrefix) {
String state = ex.getSQLState();
if (state != null) {
for (String sp : statePrefix) {
if (state.startsWith(sp))
return true;
}
}
return false;
}
/**
* Generate database + driver version diagnostics.
*
* @param md
* metadata object
* @param dbmax
* minimal DB major version number (where {@code -1} disables the
* check)
* @param dbmin
* minimal DB minor version number
* @param drmax
* minimal driver major version number (where {@code -1} disables
* the check)
* @param drmin
* minimal driver minor version number
* @param dbname
* database type
* @return diagnostics (empty when there's nothing to complain about)
*/
protected static String versionCheck(DatabaseMetaData md, int dbmax, int dbmin, int drmax, int drmin, String dbname)
throws SQLException {
StringBuilder result = new StringBuilder();
if (dbmax != -1) {
int maj = md.getDatabaseMajorVersion();
int min = md.getDatabaseMinorVersion();
if (maj < dbmax || (maj == dbmax && min < dbmin)) {
result.append(
"Unsupported " + dbname + " version: " + maj + "." + min + ", expected at least " + dbmax + "." + dbmin);
}
}
if (drmax != -1) {
int maj = md.getDriverMajorVersion();
int min = md.getDriverMinorVersion();
if (maj < drmax || (maj == drmax && min < drmin)) {
if (result.length() != 0) {
result.append(", ");
}
result.append("Unsupported " + dbname + " driver version: " + md.getDriverName() + " " + maj + "." + min
+ ", expected at least " + drmax + "." + drmin);
}
}
return result.toString();
}
/**
* Generate database version diagnostics.
*
* @param md
* metadata object
* @param dbmax
* minimal DB major version number (where {@code -1} disables the
* check)
* @param dbmin
* minimal DB minor version number
* @param dbname
* database type
* @return diagnostics (empty when there's nothing to complain about)
*/
protected static String versionCheck(DatabaseMetaData md, int dbmax, int dbmin, String dbname) throws SQLException {
return versionCheck(md, dbmax, dbmin, -1, -1, dbname);
}
/**
* Closes a {@link Statement}, logging potential problems.
* @return null
*/
protected static <T extends Statement> T closeStatement(@Nullable T stmt) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ex) {
LOG.debug("Closing statement", ex);
}
}
return null;
}
/**
* Closes a {@link ResultSet}, logging potential problems.
* @return null
*/
protected static ResultSet closeResultSet(@Nullable ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException ex) {
LOG.debug("Closing result set", ex);
}
}
return null;
}
/**
* Provides a component for a {@link PreparedStatement} and a method for
* setting the parameters within this component
*/
public interface PreparedStatementComponent {
/**
* @return a string suitable for inclusion into a
* {@link PreparedStatement}
*/
@NotNull
public String getStatementComponent();
/**
* Set the parameters need by the statement component returned by
* {@link #getStatementComponent()}
*
* @param stmt
* the statement
* @param startIndex
* of first parameter to set
* @return index of next parameter to set
* @throws SQLException
*/
public int setParameters(PreparedStatement stmt, int startIndex) throws SQLException;
}
/**
* Appends following SQL condition to the builder: {@code ID in (?,?,?)}.
* The field name {@code ID} and the number of place holders is
* configurable. If the number of place holders is greater than
* {@code maxListLength}, then the condition will have following form:
* {@code (ID in (?,?,?) or ID in (?,?,?) or ID in (?,?))}
*
* @param builder
* the condition will be appended here
* @param field
* name of the field
* @param placeholdersCount
* how many ? should be included
* @param maxListLength
* what's the max number of ? in one list
*/
protected static void appendInCondition(StringBuilder builder, String field, int placeholdersCount, int maxListLength) {
if (placeholdersCount == 1) {
builder.append(field).append(" = ?");
} else if (placeholdersCount > 0) {
if (placeholdersCount > maxListLength) {
builder.append('(');
}
int i;
for (i = 0; i < placeholdersCount / maxListLength; i++) {
if (i > 0) {
builder.append(" or ");
}
appendInCondition(builder, field, maxListLength);
}
if (placeholdersCount % maxListLength > 0) {
if (i > 0) {
builder.append(" or ");
}
appendInCondition(builder, field, placeholdersCount % maxListLength);
}
if (placeholdersCount > maxListLength) {
builder.append(')');
}
}
}
private static void appendInCondition(StringBuilder builder, String field, int placeholdersCount) {
builder.append(field).append(" in (");
Joiner.on(',').appendTo(builder, limit(cycle('?'), placeholdersCount));
builder.append(')');
}
// see <https://issues.apache.org/jira/browse/OAK-3843>
public static final int MAX_IN_CLAUSE = Integer
.getInteger("org.apache.jackrabbit.oak.plugins.document.rdb.RDBJDBCTools.MAX_IN_CLAUSE", 2048);
public static PreparedStatementComponent createInStatement(final String fieldName, final Collection<String> values,
final boolean binary) {
if (values.size() > MAX_IN_CLAUSE) {
throw new IllegalArgumentException("Maximum size of IN clause allowed is " + MAX_IN_CLAUSE + ", but " + values.size() + " was requested");
}
return new PreparedStatementComponent() {
@Override
public String getStatementComponent() {
StringBuilder sb = new StringBuilder(values.size() * 3);
// maximum "in" statement in Oracle takes 1000 values
appendInCondition(sb, fieldName, values.size(), 1000);
return sb.toString();
}
@Override
public int setParameters(PreparedStatement stmt, int startIndex) throws SQLException {
for (String value : values) {
try {
if (binary) {
stmt.setBytes(startIndex++, UTF8Encoder.encodeAsByteArray(value));
} else {
if (!UTF8Encoder.canEncode(value)) {
throw new IOException("can not encode as UTF-8");
}
stmt.setString(startIndex++, value);
}
} catch (IOException ex) {
LOG.warn("Invalid ID: " + value, ex);
throw asDocumentStoreException(ex, "Invalid ID: " + value);
}
}
return startIndex;
}
};
}
private static DocumentStoreException.Type exceptionTypeFor(Exception cause) {
return (cause instanceof SQLTransientException) ? DocumentStoreException.Type.TRANSIENT : DocumentStoreException.Type.GENERIC;
}
public static DocumentStoreException asDocumentStoreException(@NotNull Exception cause, @NotNull String message) {
return new DocumentStoreException(message, cause, exceptionTypeFor(cause));
}
}