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