/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume * Benoit Delbosc */ package org.nuxeo.ecm.core.storage.sql.jdbc; import java.io.Serializable; import java.security.MessageDigest; import java.sql.Array; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Callable; import javax.resource.ResourceException; import javax.sql.XADataSource; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.common.utils.StringUtils; import org.nuxeo.ecm.core.api.IterableQueryResult; import org.nuxeo.ecm.core.api.Lock; import org.nuxeo.ecm.core.query.QueryFilter; import org.nuxeo.ecm.core.storage.ConnectionResetException; import org.nuxeo.ecm.core.storage.PartialList; import org.nuxeo.ecm.core.storage.StorageException; import org.nuxeo.ecm.core.storage.binary.BinaryGarbageCollector; import org.nuxeo.ecm.core.storage.sql.ColumnType; import org.nuxeo.ecm.core.storage.sql.ColumnType.WrappedId; import org.nuxeo.ecm.core.storage.sql.Invalidations; import org.nuxeo.ecm.core.storage.sql.Mapper; import org.nuxeo.ecm.core.storage.sql.Model; import org.nuxeo.ecm.core.storage.sql.RepositoryImpl; import org.nuxeo.ecm.core.storage.sql.Row; import org.nuxeo.ecm.core.storage.sql.RowId; import org.nuxeo.ecm.core.storage.sql.Session.PathResolver; import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo.SQLInfoSelect; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database; import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.DialectOracle; import org.nuxeo.runtime.api.Framework; /** * A {@link JDBCMapper} maps objects to and from a JDBC database. It is specific * to a given database connection, as it computes statements. * <p> * The {@link JDBCMapper} does the mapping according to the policy defined by a * {@link Model}, and generates SQL statements recorded in the {@link SQLInfo}. */ public class JDBCMapper extends JDBCRowMapper implements Mapper { private static final Log log = LogFactory.getLog(JDBCMapper.class); public static Map<String, Serializable> testProps = new HashMap<String, Serializable>(); public static final String TEST_UPGRADE = "testUpgrade"; // property in sql.txt file public static final String TEST_UPGRADE_VERSIONS = "testUpgradeVersions"; public static final String TEST_UPGRADE_LAST_CONTRIBUTOR = "testUpgradeLastContributor"; public static final String TEST_UPGRADE_LOCKS = "testUpgradeLocks"; public static final String TEST_UPGRADE_FULLTEXT = "testUpgradeFulltext"; protected TableUpgrader tableUpgrader; private final QueryMakerService queryMakerService; private final PathResolver pathResolver; private final RepositoryImpl repository; protected boolean clusteringEnabled; /** * Creates a new Mapper. * * @param model the model * @param pathResolver the path resolver (used for startswith queries) * @param sqlInfo the sql info * @param xadatasource the XA datasource to use to get connections * @param clusterNodeHandler the cluster node handler * @param connectionPropagator the connection propagator * @param repository */ public JDBCMapper(Model model, PathResolver pathResolver, SQLInfo sqlInfo, XADataSource xadatasource, ClusterNodeHandler clusterNodeHandler, JDBCConnectionPropagator connectionPropagator, boolean noSharing, RepositoryImpl repository) throws StorageException { super(model, sqlInfo, xadatasource, clusterNodeHandler, connectionPropagator, noSharing); this.pathResolver = pathResolver; this.repository = repository; clusteringEnabled = clusterNodeHandler != null; try { queryMakerService = Framework.getService(QueryMakerService.class); } catch (Exception e) { throw new StorageException(e); } tableUpgrader = new TableUpgrader(this); tableUpgrader.add(Model.VERSION_TABLE_NAME, Model.VERSION_IS_LATEST_KEY, "upgradeVersions", TEST_UPGRADE_VERSIONS); tableUpgrader.add("dublincore", "lastContributor", "upgradeLastContributor", TEST_UPGRADE_LAST_CONTRIBUTOR); tableUpgrader.add(Model.LOCK_TABLE_NAME, Model.LOCK_OWNER_KEY, "upgradeLocks", TEST_UPGRADE_LOCKS); } @Override public int getTableSize(String tableName) { return sqlInfo.getDatabase().getTable(tableName).getColumns().size(); } /* * ----- Root ----- */ @Override public void createDatabase() throws StorageException { try { createTables(); } catch (Exception e) { checkConnectionReset(e); throw new StorageException(e); } } protected String getTableName(String origName) { if (dialect instanceof DialectOracle) { if (origName.length() > 30) { StringBuilder sb = new StringBuilder(origName.length()); try { MessageDigest digest = MessageDigest.getInstance("MD5"); sb.append(origName.substring(0, 15)); sb.append('_'); digest.update(origName.getBytes()); sb.append(Dialect.toHexString(digest.digest()).substring(0, 12)); return sb.toString(); } catch (Exception e) { throw new RuntimeException("Error", e); } } } return origName; } protected void createTables() throws SQLException { sqlInfo.executeSQLStatements(null, this); // for missing category sqlInfo.executeSQLStatements("first", this); sqlInfo.executeSQLStatements("beforeTableCreation", this); if (testProps.containsKey(TEST_UPGRADE)) { // create "old" tables sqlInfo.executeSQLStatements("testUpgrade", this); } String schemaName = dialect.getConnectionSchema(connection); DatabaseMetaData metadata = connection.getMetaData(); Set<String> tableNames = findTableNames(metadata, schemaName); Database database = sqlInfo.getDatabase(); Map<String, List<Column>> added = new HashMap<String, List<Column>>(); Statement st = null; try { st = connection.createStatement(); for (Table table : database.getTables()) { String tableName = getTableName(table.getPhysicalName()); if (tableNames.contains(tableName.toUpperCase())) { dialect.existingTableDetected(connection, table, model, sqlInfo.database); } else { /* * Create missing table. */ boolean create = dialect.preCreateTable(connection, table, model, sqlInfo.database); if (!create) { log.warn("Creation skipped for table: " + tableName); continue; } String sql = table.getCreateSql(); logger.log(sql); try { st.execute(sql); countExecute(); } catch (SQLException e) { try { closeStatement(st); } finally { throw new SQLException("Error creating table: " + sql + " : " + e.getMessage(), e); } } for (String s : table.getPostCreateSqls(model)) { logger.log(s); try { st.execute(s); countExecute(); } catch (SQLException e) { throw new SQLException( "Error post creating table: " + s + " : " + e.getMessage(), e); } } for (String s : dialect.getPostCreateTableSqls( table, model, sqlInfo.database)) { logger.log(s); try { st.execute(s); countExecute(); } catch (SQLException e) { throw new SQLException( "Error post creating table: " + s + " : " + e.getMessage(), e); } } added.put(table.getKey(), null); // null = table created } /* * Get existing columns. */ ResultSet rs = metadata.getColumns(null, schemaName, tableName, "%"); Map<String, Integer> columnTypes = new HashMap<String, Integer>(); Map<String, String> columnTypeNames = new HashMap<String, String>(); Map<String, Integer> columnTypeSizes = new HashMap<String, Integer>(); while (rs.next()) { String schema = rs.getString("TABLE_SCHEM"); if (schema != null) { // null for MySQL, doh! if ("INFORMATION_SCHEMA".equals(schema.toUpperCase())) { // H2 returns some system tables (locks) continue; } } String columnName = rs.getString("COLUMN_NAME").toUpperCase(); columnTypes.put(columnName, Integer.valueOf(rs.getInt("DATA_TYPE"))); columnTypeNames.put(columnName, rs.getString("TYPE_NAME")); columnTypeSizes.put(columnName, Integer.valueOf(rs.getInt("COLUMN_SIZE"))); } /* * Update types and create missing columns. */ List<Column> addedColumns = new LinkedList<Column>(); for (Column column : table.getColumns()) { String upperName = column.getPhysicalName().toUpperCase(); Integer type = columnTypes.remove(upperName); if (type == null) { log.warn("Adding missing column in database: " + column.getFullQuotedName()); String sql = table.getAddColumnSql(column); logger.log(sql); try { st.execute(sql); countExecute(); } catch (SQLException e) { throw new SQLException("Error adding column: " + sql + " : " + e.getMessage(), e); } for (String s : table.getPostAddSqls(column, model)) { logger.log(s); try { st.execute(s); countExecute(); } catch (SQLException e) { throw new SQLException( "Error post adding column: " + s + " : " + e.getMessage(), e); } } addedColumns.add(column); } else { int expected = column.getJdbcType(); int actual = type.intValue(); String actualName = columnTypeNames.get(upperName); Integer actualSize = columnTypeSizes.get(upperName); if (!column.setJdbcType(actual, actualName, actualSize.intValue())) { log.error(String.format( "SQL type mismatch for %s: expected %s, database has %s / %s (%s)", column.getFullQuotedName(), Integer.valueOf(expected), type, actualName, actualSize)); } } } for (String col : dialect.getIgnoredColumns(table)) { columnTypes.remove(col.toUpperCase()); } if (!columnTypes.isEmpty()) { log.warn("Database contains additional unused columns for table " + table.getQuotedName() + ": " + StringUtils.join(new ArrayList<String>( columnTypes.keySet()), ", ")); } if (!addedColumns.isEmpty()) { if (added.containsKey(table.getKey())) { throw new AssertionError(); } added.put(table.getKey(), addedColumns); } } } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } if (testProps.containsKey(TEST_UPGRADE)) { // create "old" content in tables sqlInfo.executeSQLStatements("testUpgradeOldTables", this); } // run upgrade for each table if added columns or test for (Entry<String, List<Column>> en : added.entrySet()) { List<Column> addedColumns = en.getValue(); String tableKey = en.getKey(); upgradeTable(tableKey, addedColumns); } sqlInfo.executeSQLStatements("afterTableCreation", this); sqlInfo.executeSQLStatements("last", this); dialect.performAdditionalStatements(connection); } protected void upgradeTable(String tableKey, List<Column> addedColumns) throws SQLException { tableUpgrader.upgrade(tableKey, addedColumns); } /** Finds uppercase table names. */ protected static Set<String> findTableNames(DatabaseMetaData metadata, String schemaName) throws SQLException { Set<String> tableNames = new HashSet<String>(); ResultSet rs = metadata.getTables(null, schemaName, "%", new String[] { "TABLE" }); while (rs.next()) { String tableName = rs.getString("TABLE_NAME"); tableNames.add(tableName.toUpperCase()); } return tableNames; } @Override public String createClusterNode() throws StorageException { Statement st = null; try { // get the cluster node id from the database, if necessary String sql = dialect.getClusterNodeIdSql(); String nodeId; if (sql == null) { nodeId = null; } else { st = connection.createStatement(); if (logger.isLogEnabled()) { logger.log(sql); } ResultSet rs = st.executeQuery(sql); if (!rs.next()) { throw new StorageException("Cannot get cluster node id"); } nodeId = rs.getString(1); if (logger.isLogEnabled()) { logger.log(" -> cluster node id: " + nodeId); } } // add the cluster node sqlInfo.executeSQLStatements("addClusterNode", this); return nodeId; } catch (Exception e) { checkConnectionReset(e); throw new StorageException(e); } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } @Override public void removeClusterNode() throws StorageException { try { sqlInfo.executeSQLStatements("removeClusterNode", this); } catch (Exception e) { checkConnectionReset(e); throw new StorageException(e); } } @Override public void insertClusterInvalidations(Invalidations invalidations, String nodeId) throws StorageException { String sql = dialect.getClusterInsertInvalidations(); if (nodeId != null) { sql = String.format(sql, nodeId); } List<Column> columns = sqlInfo.getClusterInvalidationsColumns(); PreparedStatement ps = null; try { ps = connection.prepareStatement(sql); int kind = Invalidations.MODIFIED; while (true) { Set<RowId> rowIds = invalidations.getKindSet(kind); // reorganize by id Map<Serializable, Set<String>> res = new HashMap<Serializable, Set<String>>(); for (RowId rowId : rowIds) { Set<String> tableNames = res.get(rowId.id); if (tableNames == null) { res.put(rowId.id, tableNames = new HashSet<String>()); } tableNames.add(rowId.tableName); } // do inserts for (Entry<Serializable, Set<String>> en : res.entrySet()) { Serializable id = en.getKey(); String fragments = join(en.getValue(), ' '); if (logger.isLogEnabled()) { logger.logSQL(sql, Arrays.<Serializable> asList(id, fragments, Long.valueOf(kind))); } Serializable frags; if (dialect.supportsArrays() && columns.get(1).getJdbcType() == Types.ARRAY) { frags = fragments.split(" "); } else { frags = fragments; } columns.get(0).setToPreparedStatement(ps, 1, id); columns.get(1).setToPreparedStatement(ps, 2, frags); columns.get(2).setToPreparedStatement(ps, 3, Long.valueOf(kind)); ps.execute(); countExecute(); } if (kind == Invalidations.MODIFIED) { kind = Invalidations.DELETED; } else { break; } } } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Could not invalidate", e); } finally { if (ps != null) { try { closeStatement(ps); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } // join that works on a set protected static final String join(Collection<String> strings, char sep) { if (strings.isEmpty()) { throw new RuntimeException(); } if (strings.size() == 1) { return strings.iterator().next(); } int size = 0; for (String word : strings) { size += word.length() + 1; } StringBuilder buf = new StringBuilder(size); for (String word : strings) { buf.append(word); buf.append(sep); } buf.setLength(size - 1); return buf.toString(); } @Override public Invalidations getClusterInvalidations(String nodeId) throws StorageException { Invalidations invalidations = new Invalidations(); String sql = dialect.getClusterGetInvalidations(); String sqldel = dialect.getClusterDeleteInvalidations(); if (nodeId != null) { sql = String.format(sql, nodeId); sqldel = String.format(sqldel, nodeId); } List<Column> columns = sqlInfo.getClusterInvalidationsColumns(); Statement st = null; try { st = connection.createStatement(); if (logger.isLogEnabled()) { logger.log(sql); } ResultSet rs = st.executeQuery(sql); countExecute(); int n = 0; while (rs.next()) { n++; Serializable id = columns.get(0).getFromResultSet(rs, 1); Serializable frags = columns.get(1).getFromResultSet(rs, 2); int kind = ((Long) columns.get(2).getFromResultSet(rs, 3)).intValue(); String[] fragments; if (dialect.supportsArrays() && frags instanceof String[]) { fragments = (String[]) frags; } else { fragments = ((String) frags).split(" "); } invalidations.add(id, fragments, kind); } if (logger.isLogEnabled()) { // logCount(n); logger.log(" -> " + invalidations); } if (dialect.isClusteringDeleteNeeded()) { if (logger.isLogEnabled()) { logger.log(sqldel); } n = st.executeUpdate(sqldel); countExecute(); if (logger.isLogEnabled()) { logger.logCount(n); } } return invalidations; } catch (Exception e) { checkConnectionReset(e, true); throw new StorageException("Could not invalidate", e); } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } @Override public Serializable getRootId(String repositoryId) throws StorageException { String sql = sqlInfo.getSelectRootIdSql(); try { if (logger.isLogEnabled()) { logger.logSQL(sql, Collections.<Serializable> singletonList(repositoryId)); } PreparedStatement ps = connection.prepareStatement(sql); try { ps.setString(1, repositoryId); ResultSet rs = ps.executeQuery(); countExecute(); if (!rs.next()) { if (logger.isLogEnabled()) { logger.log(" -> (none)"); } return null; } Column column = sqlInfo.getSelectRootIdWhatColumn(); Serializable id = column.getFromResultSet(rs, 1); if (logger.isLogEnabled()) { logger.log(" -> " + Model.MAIN_KEY + '=' + id); } // check that we didn't get several rows if (rs.next()) { throw new StorageException("Row query for " + repositoryId + " returned several rows: " + sql); } return id; } finally { closeStatement(ps); } } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Could not select: " + sql, e); } } @Override public void setRootId(Serializable repositoryId, Serializable id) throws StorageException { String sql = sqlInfo.getInsertRootIdSql(); try { PreparedStatement ps = connection.prepareStatement(sql); try { List<Column> columns = sqlInfo.getInsertRootIdColumns(); List<Serializable> debugValues = null; if (logger.isLogEnabled()) { debugValues = new ArrayList<Serializable>(2); } int i = 0; for (Column column : columns) { i++; String key = column.getKey(); Serializable v; if (key.equals(Model.MAIN_KEY)) { v = id; } else if (key.equals(Model.REPOINFO_REPONAME_KEY)) { v = repositoryId; } else { throw new RuntimeException(key); } column.setToPreparedStatement(ps, i, v); if (debugValues != null) { debugValues.add(v); } } if (debugValues != null) { logger.logSQL(sql, debugValues); debugValues.clear(); } ps.execute(); countExecute(); } finally { closeStatement(ps); } } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Could not insert: " + sql, e); } } protected QueryMaker findQueryMaker(String queryType) throws StorageException { for (Class<? extends QueryMaker> klass : queryMakerService.getQueryMakers()) { QueryMaker queryMaker; try { queryMaker = klass.newInstance(); } catch (Exception e) { throw new StorageException(e); } if (queryMaker.accepts(queryType)) { return queryMaker; } } return null; } protected void prepareUserReadAcls(QueryFilter queryFilter) throws StorageException { String sql = dialect.getPrepareUserReadAclsSql(); Serializable principals = queryFilter.getPrincipals(); if (sql == null || principals == null) { return; } if (!dialect.supportsArrays()) { principals = StringUtils.join((String[]) principals, Dialect.ARRAY_SEP); } PreparedStatement ps = null; try { ps = connection.prepareStatement(sql); if (logger.isLogEnabled()) { logger.logSQL(sql, Collections.singleton(principals)); } setToPreparedStatement(ps, 1, principals); ps.execute(); countExecute(); } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Failed to prepare user read acl cache", e); } finally { if (ps != null) { try { closeStatement(ps); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } @Override public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, boolean countTotal) throws StorageException { return query(query, queryType, queryFilter, countTotal ? -1 : 0); } @Override public PartialList<Serializable> query(String query, String queryType, QueryFilter queryFilter, long countUpTo) throws StorageException { if (dialect.needsPrepareUserReadAcls()) { prepareUserReadAcls(queryFilter); } QueryMaker queryMaker = findQueryMaker(queryType); if (queryMaker == null) { throw new StorageException("No QueryMaker accepts query: " + queryType + ": " + query); } QueryMaker.Query q = queryMaker.buildQuery(sqlInfo, model, pathResolver, query, queryFilter); if (q == null) { logger.log("Query cannot return anything due to conflicting clauses"); return new PartialList<Serializable>( Collections.<Serializable> emptyList(), 0); } long limit = queryFilter.getLimit(); long offset = queryFilter.getOffset(); if (logger.isLogEnabled()) { String sql = q.selectInfo.sql; if (limit != 0) { sql += " -- LIMIT " + limit + " OFFSET " + offset; } if (countUpTo != 0) { sql += " -- COUNT TOTAL UP TO " + countUpTo; } logger.logSQL(sql, q.selectParams); } String sql = q.selectInfo.sql; if (countUpTo == 0 && limit > 0 && dialect.supportsPaging()) { // full result set not needed for counting sql = dialect.addPagingClause(sql, limit, offset); limit = 0; offset = 0; } else if (countUpTo > 0 && dialect.supportsPaging()) { // ask one more row sql = dialect.addPagingClause(sql, Math.max(countUpTo + 1, limit + offset), 0); } PreparedStatement ps = null; try { ps = connection.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY); int i = 1; for (Serializable object : q.selectParams) { setToPreparedStatement(ps, i++, object); } ResultSet rs = ps.executeQuery(); countExecute(); // limit/offset long totalSize = -1; boolean available; if ((limit == 0) || (offset == 0)) { available = rs.first(); if (!available) { totalSize = 0; } if (limit == 0) { limit = -1; // infinite } } else { available = rs.absolute((int) offset + 1); } Column column = q.selectInfo.whatColumns.get(0); List<Serializable> ids = new LinkedList<Serializable>(); int rowNum = 0; while (available && (limit != 0)) { Serializable id = column.getFromResultSet(rs, 1); ids.add(id); rowNum = rs.getRow(); available = rs.next(); limit--; } // total size if (countUpTo != 0 && (totalSize == -1)) { if (!available && (rowNum != 0)) { // last row read was the actual last totalSize = rowNum; } else { // available if limit reached with some left // rowNum == 0 if skipped too far rs.last(); totalSize = rs.getRow(); } if (countUpTo > 0 && totalSize > countUpTo) { // the result where truncated we don't know the total size totalSize = -2; } } if (logger.isLogEnabled()) { logger.logIds(ids, countUpTo != 0, totalSize); } return new PartialList<Serializable>(ids, totalSize); } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Invalid query: " + query, e); } finally { if (ps != null) { try { closeStatement(ps); } catch (SQLException e) { log.error("Cannot close connection", e); } } } } public int setToPreparedStatement(PreparedStatement ps, int i, Serializable object) throws SQLException { if (object instanceof Calendar) { Calendar cal = (Calendar) object; ps.setTimestamp(i, dialect.getTimestampFromCalendar(cal), cal); } else if (object instanceof java.sql.Date) { ps.setDate(i, (java.sql.Date) object); } else if (object instanceof Long) { ps.setLong(i, ((Long) object).longValue()); } else if (object instanceof WrappedId) { dialect.setId(ps, i, object.toString()); } else if (object instanceof Object[]) { int jdbcType; if (object instanceof String[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.STRING).jdbcType; } else if (object instanceof Boolean[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.BOOLEAN).jdbcType; } else if (object instanceof Long[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.LONG).jdbcType; } else if (object instanceof Double[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.DOUBLE).jdbcType; } else if (object instanceof java.sql.Date[]) { jdbcType = Types.DATE; } else if (object instanceof java.sql.Clob[]) { jdbcType = Types.CLOB; } else if (object instanceof Calendar[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.TIMESTAMP).jdbcType; object = dialect.getTimestampFromCalendar((Calendar) object); } else if (object instanceof Integer[]) { jdbcType = dialect.getJDBCTypeAndString(ColumnType.INTEGER).jdbcType; } else { jdbcType = dialect.getJDBCTypeAndString(ColumnType.CLOB).jdbcType; } Array array = dialect.createArrayOf(jdbcType, (Object[]) object, connection); ps.setArray(i, array); } else { ps.setObject(i, object); } return i; } // queryFilter used for principals and permissions @Override public IterableQueryResult queryAndFetch(String query, String queryType, QueryFilter queryFilter, Object... params) throws StorageException { if (dialect.needsPrepareUserReadAcls()) { prepareUserReadAcls(queryFilter); } QueryMaker queryMaker = findQueryMaker(queryType); if (queryMaker == null) { throw new StorageException("No QueryMaker accepts query: " + queryType + ": " + query); } try { return new ResultSetQueryResult(queryMaker, query, queryFilter, pathResolver, this, params); } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Invalid query: " + queryType + ": " + query, e); } } @Override public Set<Serializable> getAncestorsIds(Collection<Serializable> ids) throws StorageException { SQLInfoSelect select = sqlInfo.getSelectAncestorsIds(); if (select == null) { return getAncestorsIdsIterative(ids); } Serializable whereIds = newIdArray(ids); Set<Serializable> res = new HashSet<Serializable>(); PreparedStatement ps = null; try { if (logger.isLogEnabled()) { logger.logSQL(select.sql, Collections.singleton(whereIds)); } Column what = select.whatColumns.get(0); ps = connection.prepareStatement(select.sql); setToPreparedStatementIdArray(ps, 1, whereIds); ResultSet rs = ps.executeQuery(); countExecute(); List<Serializable> debugIds = null; if (logger.isLogEnabled()) { debugIds = new LinkedList<Serializable>(); } while (rs.next()) { if (dialect.supportsArraysReturnInsteadOfRows()) { Serializable[] resultIds = dialect.getArrayResult(rs.getArray(1)); for (Serializable id : resultIds) { if (id != null) { res.add(id); if (logger.isLogEnabled()) { debugIds.add(id); } } } } else { Serializable id = what.getFromResultSet(rs, 1); if (id != null) { res.add(id); if (logger.isLogEnabled()) { debugIds.add(id); } } } } if (logger.isLogEnabled()) { logger.logIds(debugIds, false, 0); } return res; } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Failed to get ancestors ids", e); } finally { if (ps != null) { try { closeStatement(ps); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } /** * Uses iterative parentid selection. */ protected Set<Serializable> getAncestorsIdsIterative( Collection<Serializable> ids) throws StorageException { PreparedStatement ps = null; try { LinkedList<Serializable> todo = new LinkedList<Serializable>(ids); Set<Serializable> done = new HashSet<Serializable>(); Set<Serializable> res = new HashSet<Serializable>(); while (!todo.isEmpty()) { done.addAll(todo); SQLInfoSelect select = sqlInfo.getSelectParentIds(todo.size()); if (logger.isLogEnabled()) { logger.logSQL(select.sql, todo); } Column what = select.whatColumns.get(0); Column where = select.whereColumns.get(0); ps = connection.prepareStatement(select.sql); int i = 1; for (Serializable id : todo) { where.setToPreparedStatement(ps, i++, id); } ResultSet rs = ps.executeQuery(); countExecute(); todo = new LinkedList<Serializable>(); List<Serializable> debugIds = null; if (logger.isLogEnabled()) { debugIds = new LinkedList<Serializable>(); } while (rs.next()) { Serializable id = what.getFromResultSet(rs, 1); if (id != null) { res.add(id); if (!done.contains(id)) { todo.add(id); } if (logger.isLogEnabled()) { debugIds.add(id); } } } if (logger.isLogEnabled()) { logger.logIds(debugIds, false, 0); } ps.close(); ps = null; } return res; } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Failed to get ancestors ids", e); } finally { if (ps != null) { try { closeStatement(ps); } catch (SQLException e) { log.error(e.getMessage(), e); } } } } @Override public void updateReadAcls() throws StorageException { if (!dialect.supportsReadAcl()) { return; } if (log.isDebugEnabled()) { log.debug("updateReadAcls: updating"); } Statement st = null; try { st = connection.createStatement(); String sql = dialect.getUpdateReadAclsSql(); if (logger.isLogEnabled()) { logger.log(sql); } st.execute(sql); countExecute(); } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Failed to update read acls", e); } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } if (log.isDebugEnabled()) { log.debug("updateReadAcls: done."); } } @Override public void rebuildReadAcls() throws StorageException { if (!dialect.supportsReadAcl()) { return; } log.debug("rebuildReadAcls: rebuilding ..."); Statement st = null; try { st = connection.createStatement(); String sql = dialect.getRebuildReadAclsSql(); logger.log(sql); st.execute(sql); countExecute(); } catch (Exception e) { checkConnectionReset(e); throw new StorageException("Failed to rebuild read acls", e); } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } log.debug("rebuildReadAcls: done."); } /* * ----- Locking ----- */ protected Connection connection(boolean autocommit) throws StorageException { checkConnectionValid(); try { connection.setAutoCommit(autocommit); } catch (SQLException e) { throw new StorageException("Cannot set auto commit mode onto " + this + "'s connection", e); } return connection; } /** * Calls the callable, inside a transaction if in cluster mode. * <p> * Called under {@link #serializationLock}. */ protected Lock callInTransaction(Callable<Lock> callable, boolean tx) throws StorageException { boolean ok = false; checkConnectionValid(); try { if (log.isDebugEnabled()) { log.debug("callInTransaction setAutoCommit " + !tx); } connection.setAutoCommit(!tx); } catch (SQLException e) { throw new StorageException("Cannot set auto commit mode onto " + this + "'s connection", e); } try { Lock result; try { result = callable.call(); } catch (StorageException e) { throw e; } catch (Exception e) { throw new StorageException(e); } ok = true; return result; } finally { if (tx) { try { try { if (ok) { if (log.isDebugEnabled()) { log.debug("callInTransaction commit"); } connection.commit(); } else { if (log.isDebugEnabled()) { log.debug("callInTransaction rollback"); } connection.rollback(); } } finally { // restore autoCommit=true if (log.isDebugEnabled()) { log.debug("callInTransaction restoring autoCommit=true"); } connection.setAutoCommit(true); } } catch (SQLException e) { throw new StorageException(e); } } } } @Override public Lock getLock(Serializable id) throws StorageException { if (log.isDebugEnabled()) { try { log.debug("getLock " + id + " while autoCommit=" + connection.getAutoCommit()); } catch (SQLException e) { throw new RuntimeException(e); } } RowId rowId = new RowId(Model.LOCK_TABLE_NAME, id); Row row; try { row = readSimpleRow(rowId); } catch (ConnectionResetException e) { // retry once row = readSimpleRow(rowId); } return row == null ? null : new Lock( (String) row.get(Model.LOCK_OWNER_KEY), (Calendar) row.get(Model.LOCK_CREATED_KEY)); } @Override public Lock setLock(final Serializable id, final Lock lock) throws StorageException { if (log.isDebugEnabled()) { log.debug("setLock " + id + " owner=" + lock.getOwner()); } SetLock call = new SetLock(id, lock); return callInTransaction(call, clusteringEnabled); } protected class SetLock implements Callable<Lock> { protected final Serializable id; protected final Lock lock; protected SetLock(Serializable id, Lock lock) { super(); this.id = id; this.lock = lock; } @Override public Lock call() throws StorageException { Lock oldLock = getLock(id); if (oldLock == null) { Row row = new Row(Model.LOCK_TABLE_NAME, id); row.put(Model.LOCK_OWNER_KEY, lock.getOwner()); row.put(Model.LOCK_CREATED_KEY, lock.getCreated()); insertSimpleRows(Model.LOCK_TABLE_NAME, Collections.singletonList(row)); } return oldLock; } } @Override public Lock removeLock(final Serializable id, final String owner, final boolean force) throws StorageException { if (log.isDebugEnabled()) { log.debug("removeLock " + id + " owner=" + owner + " force=" + force); } RemoveLock call = new RemoveLock(id, owner, force); return callInTransaction(call, !force); } protected class RemoveLock implements Callable<Lock> { protected final Serializable id; protected final String owner; protected final boolean force; protected RemoveLock(Serializable id, String owner, boolean force) { super(); this.id = id; this.owner = owner; this.force = force; } @Override public Lock call() throws StorageException { Lock oldLock = force ? null : getLock(id); if (!force && owner != null) { if (oldLock == null) { // not locked, nothing to do return null; } if (!repository.getLockManager().canLockBeRemoved(oldLock, owner)) { // existing mismatched lock, flag failure return new Lock(oldLock, true); } } if (force || oldLock != null) { deleteRows(Model.LOCK_TABLE_NAME, Collections.singleton(id)); } return oldLock; } } @Override public void markReferencedBinaries(BinaryGarbageCollector gc) throws StorageException { log.debug("Starting binaries GC mark"); Statement st = null; try { st = connection.createStatement(); int i = -1; for (String sql : sqlInfo.getBinariesSql) { i++; Column col = sqlInfo.getBinariesColumns.get(i); if (logger.isLogEnabled()) { logger.log(sql); } ResultSet rs = st.executeQuery(sql); countExecute(); int n = 0; while (rs.next()) { n++; String digest = (String) col.getFromResultSet(rs, 1); if (digest != null) { gc.mark(digest); } } if (logger.isLogEnabled()) { logger.logCount(n); } } } catch (Exception e) { checkConnectionReset(e); throw new RuntimeException("Failed to mark binaries for gC", e); } finally { if (st != null) { try { closeStatement(st); } catch (SQLException e) { log.error(e.getMessage(), e); } } } log.debug("End of binaries GC mark"); } /* * ----- XAResource ----- */ protected static String systemToString(Object o) { return o.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(o)); } @Override public void start(Xid xid, int flags) throws XAException { try { checkConnectionValid(); xaresource.start(xid, flags); if (logger.isLogEnabled()) { logger.log("XA start on " + systemToString(xid)); } } catch (StorageException e) { throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); } catch (XAException e) { checkConnectionReset(e); logger.error("XA start error on " + systemToString(xid), e); throw e; } } @Override public void end(Xid xid, int flags) throws XAException { try { xaresource.end(xid, flags); if (logger.isLogEnabled()) { logger.log("XA end on " + systemToString(xid)); } } catch (NullPointerException e) { // H2 when no active transaction logger.error("XA end error on " + systemToString(xid), e); throw (XAException) new XAException(XAException.XAER_RMERR).initCause(e); } catch (XAException e) { if (flags != XAResource.TMFAIL) { logger.error("XA end error on " + systemToString(xid), e); } throw e; } } @Override public int prepare(Xid xid) throws XAException { try { return xaresource.prepare(xid); } catch (XAException e) { logger.error("XA prepare error on " + systemToString(xid), e); throw e; } } @Override public void commit(Xid xid, boolean onePhase) throws XAException { try { xaresource.commit(xid, onePhase); } catch (XAException e) { logger.error("XA commit error on " + systemToString(xid), e); throw e; } } // rollback interacts with caches so is in RowMapper @Override public void forget(Xid xid) throws XAException { xaresource.forget(xid); } @Override public Xid[] recover(int flag) throws XAException { return xaresource.recover(flag); } @Override public boolean setTransactionTimeout(int seconds) throws XAException { return xaresource.setTransactionTimeout(seconds); } @Override public int getTransactionTimeout() throws XAException { return xaresource.getTransactionTimeout(); } @Override public boolean isSameRM(XAResource xares) throws XAException { throw new UnsupportedOperationException(); } @Override public boolean isConnected() { return connection != null; } @Override public void connect() throws StorageException { openConnections(); } @Override public void disconnect() { closeConnections(); } }