/* * 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.core.persistence.db; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.id.PropertyId; import org.apache.jackrabbit.core.fs.FileSystem; import org.apache.jackrabbit.core.fs.local.LocalFileSystem; import org.apache.jackrabbit.core.persistence.AbstractPersistenceManager; import org.apache.jackrabbit.core.persistence.PMContext; import org.apache.jackrabbit.core.persistence.util.BLOBStore; import org.apache.jackrabbit.core.persistence.util.FileSystemBLOBStore; import org.apache.jackrabbit.core.persistence.util.Serializer; import org.apache.jackrabbit.core.state.ChangeLog; import org.apache.jackrabbit.core.state.ItemState; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.core.state.NoSuchItemStateException; import org.apache.jackrabbit.core.state.NodeReferences; import org.apache.jackrabbit.core.state.NodeState; import org.apache.jackrabbit.core.state.PropertyState; import org.apache.jackrabbit.core.value.InternalValue; import org.apache.jackrabbit.util.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.PropertyType; import javax.jcr.RepositoryException; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; 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.util.HashMap; import java.util.Iterator; import java.util.Map; /** * Abstract base class for database persistence managers. This class * contains common functionality for database persistence manager subclasses * that normally differ only in the way the database connection is acquired. * Subclasses should override the {@link #getConnection()} method to return * the configured database connection. * <p> * See the {@link SimpleDbPersistenceManager} for a detailed description * of the available configuration options and database behaviour. * * @deprecated Please migrate to a bundle persistence manager * (<a href="https://issues.apache.org/jira/browse/JCR-2802">JCR-2802</a>) */ @Deprecated public abstract class DatabasePersistenceManager extends AbstractPersistenceManager { /** * Logger instance */ private static Logger log = LoggerFactory.getLogger(DatabasePersistenceManager.class); protected static final String SCHEMA_OBJECT_PREFIX_VARIABLE = "${schemaObjectPrefix}"; protected boolean initialized; protected String schema; protected String schemaObjectPrefix; protected boolean externalBLOBs; /** * Whether the schema check must be done during initialization. */ private boolean schemaCheckEnabled = true; // initial size of buffer used to serialize objects protected static final int INITIAL_BUFFER_SIZE = 1024; // jdbc connection protected Connection con; // internal flag governing whether an automatic reconnect should be // attempted after a SQLException had been encountered protected boolean autoReconnect = true; // time to sleep in ms before a reconnect is attempted protected static final int SLEEP_BEFORE_RECONNECT = 10000; // the map of prepared statements (key: sql stmt, value: prepared stmt) private Map<String, PreparedStatement> preparedStatements = new HashMap<String, PreparedStatement>(); // SQL statements for NodeState management protected String nodeStateInsertSQL; protected String nodeStateUpdateSQL; protected String nodeStateSelectSQL; protected String nodeStateSelectExistSQL; protected String nodeStateDeleteSQL; // SQL statements for PropertyState management protected String propertyStateInsertSQL; protected String propertyStateUpdateSQL; protected String propertyStateSelectSQL; protected String propertyStateSelectExistSQL; protected String propertyStateDeleteSQL; // SQL statements for NodeReference management protected String nodeReferenceInsertSQL; protected String nodeReferenceUpdateSQL; protected String nodeReferenceSelectSQL; protected String nodeReferenceSelectExistSQL; protected String nodeReferenceDeleteSQL; // SQL statements for BLOB management // (if <code>externalBLOBs==false</code>) protected String blobInsertSQL; protected String blobUpdateSQL; protected String blobSelectSQL; protected String blobSelectExistSQL; protected String blobDeleteSQL; /** * file system where BLOB data is stored * (if <code>externalBLOBs==true</code>) */ protected FileSystem blobFS; /** * BLOBStore that manages BLOB data in the file system * (if <code>externalBLOBs==true</code>) */ protected BLOBStore blobStore; /** * Creates a new <code>DatabasePersistenceManager</code> instance. */ public DatabasePersistenceManager() { schema = "default"; schemaObjectPrefix = ""; externalBLOBs = true; initialized = false; } //----------------------------------------------------< setters & getters > public String getSchemaObjectPrefix() { return schemaObjectPrefix; } public void setSchemaObjectPrefix(String schemaObjectPrefix) { // make sure prefix is all uppercase this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase(); } public String getSchema() { return schema; } public void setSchema(String schema) { this.schema = schema; } public boolean isExternalBLOBs() { return externalBLOBs; } public void setExternalBLOBs(boolean externalBLOBs) { this.externalBLOBs = externalBLOBs; } public void setExternalBLOBs(String externalBLOBs) { this.externalBLOBs = Boolean.valueOf(externalBLOBs).booleanValue(); } /** * @return whether the schema check is enabled */ public final boolean isSchemaCheckEnabled() { return schemaCheckEnabled; } /** * @param enabled set whether the schema check is enabled */ public final void setSchemaCheckEnabled(boolean enabled) { schemaCheckEnabled = enabled; } //---------------------------------------------------< PersistenceManager > /** * {@inheritDoc} */ public void init(PMContext context) throws Exception { if (initialized) { throw new IllegalStateException("already initialized"); } // setup jdbc connection initConnection(); DatabaseMetaData meta = con.getMetaData(); try { log.info("Database: " + meta.getDatabaseProductName() + " / " + meta.getDatabaseProductVersion()); log.info("Driver: " + meta.getDriverName() + " / " + meta.getDriverVersion()); } catch (SQLException e) { log.warn("Can not retrieve database and driver name / version", e); } // make sure schemaObjectPrefix consists of legal name characters only prepareSchemaObjectPrefix(); // check if schema objects exist and create them if necessary if (isSchemaCheckEnabled()) { checkSchema(); } // build sql statements buildSQLStatements(); // prepare statements initPreparedStatements(); if (externalBLOBs) { /** * store BLOBs in local file system in a sub directory * of the workspace home directory */ LocalFileSystem blobFS = new LocalFileSystem(); blobFS.setRoot(new File(context.getHomeDir(), "blobs")); blobFS.init(); this.blobFS = blobFS; blobStore = new FileSystemBLOBStore(blobFS); } else { /** * store BLOBs in db */ blobStore = new DbBLOBStore(); } initialized = true; } /** * {@inheritDoc} */ public synchronized void close() throws Exception { if (!initialized) { throw new IllegalStateException("not initialized"); } try { // close shared prepared statements for (PreparedStatement ps : preparedStatements.values()) { closeStatement(ps); } preparedStatements.clear(); if (externalBLOBs) { // close BLOB file system blobFS.close(); blobFS = null; } blobStore = null; // close jdbc connection closeConnection(con); } finally { initialized = false; } } /** * {@inheritDoc} */ public synchronized void store(ChangeLog changeLog) throws ItemStateException { // temporarily disable automatic reconnect feature // since the changes need to be persisted atomically autoReconnect = false; try { ItemStateException ise = null; // number of attempts to store the changes int trials = 2; while (trials > 0) { try { super.store(changeLog); break; } catch (ItemStateException e) { // catch exception and fall through... ise = e; } if (ise != null && ise.getCause() instanceof SQLException) { // a SQLException has been thrown if (--trials > 0) { // try to reconnect log.warn("storing changes failed, about to reconnect...", ise.getCause()); // try to reconnect if (reestablishConnection()) { // now let's give it another try ise = null; continue; } else { // reconnect failed, proceed with error processing break; } } } else { // a non-SQLException has been thrown, // proceed with error processing break; } } if (ise == null) { // storing the changes succeeded, now commit the changes try { con.commit(); } catch (SQLException e) { String msg = "committing change log failed"; log.error(msg, e); throw new ItemStateException(msg, e); } } else { // storing the changes failed, rollback changes try { con.rollback(); } catch (SQLException e) { String msg = "rollback of change log failed"; log.error(msg, e); } // re-throw original exception throw ise; } } finally { // re-enable automatic reconnect feature autoReconnect = true; } } /** * {@inheritDoc} */ public NodeState load(NodeId id) throws NoSuchItemStateException, ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (nodeStateSelectSQL) { ResultSet rs = null; InputStream in = null; try { Statement stmt = executeStmt(nodeStateSelectSQL, new Object[]{id.toString()}); rs = stmt.getResultSet(); if (!rs.next()) { throw new NoSuchItemStateException(id.toString()); } in = rs.getBinaryStream(1); NodeState state = createNew(id); Serializer.deserialize(state, in); return state; } catch (Exception e) { if (e instanceof NoSuchItemStateException) { throw (NoSuchItemStateException) e; } String msg = "failed to read node state: " + id; log.error(msg, e); throw new ItemStateException(msg, e); } finally { IOUtils.closeQuietly(in); closeResultSet(rs); } } } /** * {@inheritDoc} */ public PropertyState load(PropertyId id) throws NoSuchItemStateException, ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (propertyStateSelectSQL) { ResultSet rs = null; InputStream in = null; try { Statement stmt = executeStmt(propertyStateSelectSQL, new Object[]{id.toString()}); rs = stmt.getResultSet(); if (!rs.next()) { throw new NoSuchItemStateException(id.toString()); } in = rs.getBinaryStream(1); if (!externalBLOBs) { // JCR-1532: pre-fetch/buffer stream data ByteArrayInputStream bain = new ByteArrayInputStream( IOUtils.toByteArray(in)); IOUtils.closeQuietly(in); in = bain; } PropertyState state = createNew(id); Serializer.deserialize(state, in, blobStore); return state; } catch (Exception e) { if (e instanceof NoSuchItemStateException) { throw (NoSuchItemStateException) e; } String msg = "failed to read property state: " + id; log.error(msg, e); throw new ItemStateException(msg, e); } finally { IOUtils.closeQuietly(in); closeResultSet(rs); } } } /** * {@inheritDoc} * <p> * This method uses shared <code>PreparedStatement</code>s which must * be executed strictly sequentially. Because this method synchronizes on * the persistence manager instance there is no need to synchronize on the * shared statement. If the method would not be synchronized the shared * statements would have to be synchronized. */ @Override public synchronized void store(NodeState state) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } // check if insert or update boolean update = state.getStatus() != ItemState.STATUS_NEW; //boolean update = exists(state.getId()); String sql = (update) ? nodeStateUpdateSQL : nodeStateInsertSQL; try { ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); // serialize node state Serializer.serialize(state, out); // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(sql, new Object[]{out.toByteArray(), state.getNodeId().toString()}); // there's no need to close a ByteArrayOutputStream //out.close(); } catch (Exception e) { String msg = "failed to write node state: " + state.getNodeId(); log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} * <p> * This method uses shared <code>PreparedStatement</code>s which must * be executed strictly sequentially. Because this method synchronizes on * the persistence manager instance there is no need to synchronize on the * shared statement. If the method would not be synchronized the shared * statements would have to be synchronized. */ @Override public synchronized void store(PropertyState state) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } // check if insert or update boolean update = state.getStatus() != ItemState.STATUS_NEW; //boolean update = exists(state.getId()); String sql = (update) ? propertyStateUpdateSQL : propertyStateInsertSQL; try { ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); // serialize property state Serializer.serialize(state, out, blobStore); // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(sql, new Object[]{out.toByteArray(), state.getPropertyId().toString()}); // there's no need to close a ByteArrayOutputStream //out.close(); } catch (Exception e) { String msg = "failed to write property state: " + state.getPropertyId(); log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} */ public synchronized void destroy(NodeState state) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } try { // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(nodeStateDeleteSQL, new Object[]{state.getNodeId().toString()}); } catch (Exception e) { String msg = "failed to delete node state: " + state.getNodeId(); log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} */ public synchronized void destroy(PropertyState state) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } // make sure binary values (BLOBs) are properly removed InternalValue[] values = state.getValues(); if (values != null) { for (int i = 0; i < values.length; i++) { InternalValue val = values[i]; if (val != null) { if (val.getType() == PropertyType.BINARY) { val.deleteBinaryResource(); // also remove from BLOBStore String blobId = blobStore.createId(state.getPropertyId(), i); try { blobStore.remove(blobId); } catch (Exception e) { log.warn("failed to remove from BLOBStore: " + blobId, e); } } } } } try { // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(propertyStateDeleteSQL, new Object[]{state.getPropertyId().toString()}); } catch (Exception e) { String msg = "failed to delete property state: " + state.getPropertyId(); log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} */ public NodeReferences loadReferencesTo(NodeId targetId) throws NoSuchItemStateException, ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (nodeReferenceSelectSQL) { ResultSet rs = null; InputStream in = null; try { Statement stmt = executeStmt( nodeReferenceSelectSQL, new Object[]{targetId.toString()}); rs = stmt.getResultSet(); if (!rs.next()) { throw new NoSuchItemStateException(targetId.toString()); } in = rs.getBinaryStream(1); NodeReferences refs = new NodeReferences(targetId); Serializer.deserialize(refs, in); return refs; } catch (Exception e) { if (e instanceof NoSuchItemStateException) { throw (NoSuchItemStateException) e; } String msg = "failed to read node references: " + targetId; log.error(msg, e); throw new ItemStateException(msg, e); } finally { IOUtils.closeQuietly(in); closeResultSet(rs); } } } /** * {@inheritDoc} * <p> * This method uses shared <code>PreparedStatement</code>s which must * be executed strictly sequentially. Because this method synchronizes on * the persistence manager instance there is no need to synchronize on the * shared statement. If the method would not be synchronized the shared * statements would have to be synchronized. */ @Override public synchronized void store(NodeReferences refs) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } // check if insert or update boolean update = existsReferencesTo(refs.getTargetId()); String sql = (update) ? nodeReferenceUpdateSQL : nodeReferenceInsertSQL; try { ByteArrayOutputStream out = new ByteArrayOutputStream(INITIAL_BUFFER_SIZE); // serialize references Serializer.serialize(refs, out); // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(sql, new Object[]{out.toByteArray(), refs.getTargetId().toString()}); // there's no need to close a ByteArrayOutputStream //out.close(); } catch (Exception e) { String msg = "failed to write " + refs; log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} */ public synchronized void destroy(NodeReferences refs) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } try { // we are synchronized on this instance, therefore we do not // not have to additionally synchronize on the sql statement executeStmt(nodeReferenceDeleteSQL, new Object[]{refs.getTargetId().toString()}); } catch (Exception e) { String msg = "failed to delete " + refs; log.error(msg, e); throw new ItemStateException(msg, e); } } /** * {@inheritDoc} */ public boolean exists(NodeId id) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (nodeStateSelectExistSQL) { ResultSet rs = null; try { Statement stmt = executeStmt(nodeStateSelectExistSQL, new Object[]{id.toString()}); rs = stmt.getResultSet(); // a node state exists if the result has at least one entry return rs.next(); } catch (Exception e) { String msg = "failed to check existence of node state: " + id; log.error(msg, e); throw new ItemStateException(msg, e); } finally { closeResultSet(rs); } } } /** * {@inheritDoc} */ public boolean exists(PropertyId id) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (propertyStateSelectExistSQL) { ResultSet rs = null; try { Statement stmt = executeStmt( propertyStateSelectExistSQL, new Object[]{id.toString()}); rs = stmt.getResultSet(); // a property state exists if the result has at least one entry return rs.next(); } catch (Exception e) { String msg = "failed to check existence of property state: " + id; log.error(msg, e); throw new ItemStateException(msg, e); } finally { closeResultSet(rs); } } } /** * {@inheritDoc} */ public boolean existsReferencesTo(NodeId targetId) throws ItemStateException { if (!initialized) { throw new IllegalStateException("not initialized"); } synchronized (nodeReferenceSelectExistSQL) { ResultSet rs = null; try { Statement stmt = executeStmt( nodeReferenceSelectExistSQL, new Object[]{targetId.toString()}); rs = stmt.getResultSet(); // a reference exists if the result has at least one entry return rs.next(); } catch (Exception e) { String msg = "failed to check existence of node references: " + targetId; log.error(msg, e); throw new ItemStateException(msg, e); } finally { closeResultSet(rs); } } } //----------------------------------< misc. helper methods & overridables > /** * Initializes the database connection used by this persistence manager. * <p> * Subclasses should normally override the {@link #getConnection()} * method instead of this one. The default implementation calls * {@link #getConnection()} to get the database connection and disables * the autocommit feature. * * @throws Exception if an error occurs */ protected void initConnection() throws Exception { con = getConnection(); // JCR-1013: Setter may fail unnecessarily on a managed connection if (con.getAutoCommit()) { con.setAutoCommit(false); } } /** * Abstract factory method for creating a new database connection. This * method is called by {@link #init(PMContext)} when the persistence * manager is started. The returned connection should come with the default * JDBC settings, as the {@link #init(PMContext)} method will explicitly * set the <code>autoCommit</code> and other properties as needed. * <p> * Note that the returned database connection is kept during the entire * lifetime of the persistence manager, after which it is closed by * {@link #close()} using the {@link #closeConnection(Connection)} method. * * @return new connection * @throws Exception if an error occurs */ protected Connection getConnection() throws Exception { throw new UnsupportedOperationException("Override in a subclass!"); } /** * Closes the given database connection. This method is called by * {@link #close()} to close the connection acquired using * {@link #getConnection()} when the persistence manager was started. * <p> * The default implementation just calls the {@link Connection#close()} * method of the given connection, but subclasses can override this * method to provide more extensive database and connection cleanup. * * @param connection database connection * @throws Exception if an error occurs */ protected void closeConnection(Connection connection) throws Exception { connection.close(); } /** * Re-establishes the database connection. This method is called by * {@link #store(ChangeLog)} and {@link #executeStmt(String, Object[])} * after a <code>SQLException</code> had been encountered. * @return true if the connection could be successfully re-established, * false otherwise. */ protected synchronized boolean reestablishConnection() { // in any case try to shut down current connection // gracefully in order to avoid potential memory leaks // close shared prepared statements for (Iterator<PreparedStatement> it = preparedStatements.values().iterator(); it.hasNext();) { PreparedStatement stmt = it.next(); if (stmt != null) { try { stmt.close(); } catch (SQLException se) { // ignored, see JCR-765 } } } try { closeConnection(con); } catch (Exception ignore) { } // sleep for a while to give database a chance // to restart before a reconnect is attempted try { Thread.sleep(SLEEP_BEFORE_RECONNECT); } catch (InterruptedException ignore) { } // now try to re-establish connection try { initConnection(); initPreparedStatements(); return true; } catch (Exception e) { log.error("failed to re-establish connection", e); // reconnect failed return false; } } /** * Executes the given SQL statement with the specified parameters. * If a <code>SQLException</code> is encountered and * <code>autoReconnect==true</code> <i>one</i> attempt is made to re-establish * the database connection and re-execute the statement. * * @param sql statement to execute * @param params parameters to set * @return the <code>Statement</code> object that had been executed * @throws SQLException if an error occurs */ protected Statement executeStmt(String sql, Object[] params) throws SQLException { int trials = autoReconnect ? 2 : 1; while (true) { PreparedStatement stmt = (PreparedStatement) preparedStatements.get(sql); try { for (int i = 0; i < params.length; i++) { if (params[i] instanceof SizedInputStream) { SizedInputStream in = (SizedInputStream) params[i]; stmt.setBinaryStream(i + 1, in, (int) in.getSize()); } else { stmt.setObject(i + 1, params[i]); } } stmt.execute(); resetStatement(stmt); return stmt; } catch (SQLException se) { if (--trials == 0) { // no more trials, re-throw throw se; } log.warn("execute failed, about to reconnect... {}", se.getMessage()); // try to reconnect if (reestablishConnection()) { // reconnect succeeded; check whether it's possible to // re-execute the prepared stmt with the given parameters for (int i = 0; i < params.length; i++) { if (params[i] instanceof SizedInputStream) { SizedInputStream in = (SizedInputStream) params[i]; if (in.isConsumed()) { // we're unable to re-execute the prepared stmt // since an InputStream paramater has already // been 'consumed'; // re-throw previous SQLException throw se; } } } // try again to execute the statement continue; } else { // reconnect failed, re-throw previous SQLException throw se; } } } } /** * Resets the given <code>PreparedStatement</code> by clearing the parameters * and warnings contained. * <p> * NOTE: This method MUST be called in a synchronized context as neither * this method nor the <code>PreparedStatement</code> instance on which it * operates are thread safe. * * @param stmt The <code>PreparedStatement</code> to reset. If * <code>null</code> this method does nothing. */ protected void resetStatement(PreparedStatement stmt) { if (stmt != null) { try { stmt.clearParameters(); stmt.clearWarnings(); } catch (SQLException se) { logException("failed resetting PreparedStatement", se); } } } protected void closeResultSet(ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException se) { logException("failed closing ResultSet", se); } } } protected void closeStatement(Statement stmt) { if (stmt != null) { try { stmt.close(); } catch (SQLException se) { logException("failed closing Statement", se); } } } protected void logException(String message, SQLException se) { if (message != null) { log.error(message); } log.error(" reason: " + se.getMessage()); log.error("state/code: " + se.getSQLState() + "/" + se.getErrorCode()); log.debug(" dump:", se); } /** * Makes sure that <code>schemaObjectPrefix</code> does only consist of * characters that are allowed in names on the target database. Illegal * characters will be escaped as necessary. * * @throws Exception if an error occurs */ protected void prepareSchemaObjectPrefix() throws Exception { DatabaseMetaData metaData = con.getMetaData(); String legalChars = metaData.getExtraNameCharacters(); legalChars += "ABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_"; String prefix = schemaObjectPrefix.toUpperCase(); StringBuilder escaped = new StringBuilder(); for (int i = 0; i < prefix.length(); i++) { char c = prefix.charAt(i); if (legalChars.indexOf(c) == -1) { escaped.append("_x"); String hex = Integer.toHexString(c); escaped.append("0000".toCharArray(), 0, 4 - hex.length()); escaped.append(hex); escaped.append("_"); } else { escaped.append(c); } } schemaObjectPrefix = escaped.toString(); } /** * Checks if the required schema objects exist and creates them if they * don't exist yet. * * @throws Exception if an error occurs */ protected void checkSchema() throws Exception { DatabaseMetaData metaData = con.getMetaData(); String tableName = schemaObjectPrefix + "NODE"; if (metaData.storesLowerCaseIdentifiers()) { tableName = tableName.toLowerCase(); } else if (metaData.storesUpperCaseIdentifiers()) { tableName = tableName.toUpperCase(); } ResultSet rs = metaData.getTables(null, null, tableName, null); boolean schemaExists; try { schemaExists = rs.next(); } finally { rs.close(); } if (!schemaExists) { // read ddl from resources InputStream in = getSchemaDDL(); if (in == null) { String msg = "Configuration error: unknown schema '" + schema + "'"; log.debug(msg); throw new RepositoryException(msg); } BufferedReader reader = new BufferedReader(new InputStreamReader(in)); Statement stmt = con.createStatement(); try { String sql = reader.readLine(); while (sql != null) { // Skip comments and empty lines if (!sql.startsWith("#") && sql.length() > 0) { // replace prefix variable sql = createSchemaSql(sql); // execute sql stmt stmt.executeUpdate(sql); } // read next sql stmt sql = reader.readLine(); } // commit the changes con.commit(); } finally { IOUtils.closeQuietly(in); closeStatement(stmt); } } } /** * Replace wildcards and return the expanded SQL statement. * * @param sql The SQL with embedded wildcards. * @return The SQL with no wildcards present. */ protected String createSchemaSql(String sql) { // replace prefix variable return Text.replace(sql, SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix); } /** * Returns an input stream to the schema DDL resource. * @return an input stream to the schema DDL resource. */ protected InputStream getSchemaDDL() { // JCR-595: Use the class explicitly instead of using getClass() // to avoid problems when subclassed in a different package return DatabasePersistenceManager.class.getResourceAsStream(schema + ".ddl"); } /** * Builds the SQL statements */ protected void buildSQLStatements() { nodeStateInsertSQL = "insert into " + schemaObjectPrefix + "NODE (NODE_DATA, NODE_ID) values (?, ?)"; nodeStateUpdateSQL = "update " + schemaObjectPrefix + "NODE set NODE_DATA = ? where NODE_ID = ?"; nodeStateSelectSQL = "select NODE_DATA from " + schemaObjectPrefix + "NODE where NODE_ID = ?"; nodeStateSelectExistSQL = "select 1 from " + schemaObjectPrefix + "NODE where NODE_ID = ?"; nodeStateDeleteSQL = "delete from " + schemaObjectPrefix + "NODE where NODE_ID = ?"; propertyStateInsertSQL = "insert into " + schemaObjectPrefix + "PROP (PROP_DATA, PROP_ID) values (?, ?)"; propertyStateUpdateSQL = "update " + schemaObjectPrefix + "PROP set PROP_DATA = ? where PROP_ID = ?"; propertyStateSelectSQL = "select PROP_DATA from " + schemaObjectPrefix + "PROP where PROP_ID = ?"; propertyStateSelectExistSQL = "select 1 from " + schemaObjectPrefix + "PROP where PROP_ID = ?"; propertyStateDeleteSQL = "delete from " + schemaObjectPrefix + "PROP where PROP_ID = ?"; nodeReferenceInsertSQL = "insert into " + schemaObjectPrefix + "REFS (REFS_DATA, NODE_ID) values (?, ?)"; nodeReferenceUpdateSQL = "update " + schemaObjectPrefix + "REFS set REFS_DATA = ? where NODE_ID = ?"; nodeReferenceSelectSQL = "select REFS_DATA from " + schemaObjectPrefix + "REFS where NODE_ID = ?"; nodeReferenceSelectExistSQL = "select 1 from " + schemaObjectPrefix + "REFS where NODE_ID = ?"; nodeReferenceDeleteSQL = "delete from " + schemaObjectPrefix + "REFS where NODE_ID = ?"; if (!externalBLOBs) { blobInsertSQL = "insert into " + schemaObjectPrefix + "BINVAL (BINVAL_DATA, BINVAL_ID) values (?, ?)"; blobUpdateSQL = "update " + schemaObjectPrefix + "BINVAL set BINVAL_DATA = ? where BINVAL_ID = ?"; blobSelectSQL = "select BINVAL_DATA from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; blobSelectExistSQL = "select 1 from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; blobDeleteSQL = "delete from " + schemaObjectPrefix + "BINVAL where BINVAL_ID = ?"; } } /** * Initializes the map of prepared statements. * * @throws SQLException if an error occurs */ protected void initPreparedStatements() throws SQLException { preparedStatements.put( nodeStateInsertSQL, con.prepareStatement(nodeStateInsertSQL)); preparedStatements.put( nodeStateUpdateSQL, con.prepareStatement(nodeStateUpdateSQL)); preparedStatements.put( nodeStateSelectSQL, con.prepareStatement(nodeStateSelectSQL)); preparedStatements.put( nodeStateSelectExistSQL, con.prepareStatement(nodeStateSelectExistSQL)); preparedStatements.put( nodeStateDeleteSQL, con.prepareStatement(nodeStateDeleteSQL)); preparedStatements.put( propertyStateInsertSQL, con.prepareStatement(propertyStateInsertSQL)); preparedStatements.put( propertyStateUpdateSQL, con.prepareStatement(propertyStateUpdateSQL)); preparedStatements.put( propertyStateSelectSQL, con.prepareStatement(propertyStateSelectSQL)); preparedStatements.put( propertyStateSelectExistSQL, con.prepareStatement(propertyStateSelectExistSQL)); preparedStatements.put( propertyStateDeleteSQL, con.prepareStatement(propertyStateDeleteSQL)); preparedStatements.put( nodeReferenceInsertSQL, con.prepareStatement(nodeReferenceInsertSQL)); preparedStatements.put( nodeReferenceUpdateSQL, con.prepareStatement(nodeReferenceUpdateSQL)); preparedStatements.put( nodeReferenceSelectSQL, con.prepareStatement(nodeReferenceSelectSQL)); preparedStatements.put( nodeReferenceSelectExistSQL, con.prepareStatement(nodeReferenceSelectExistSQL)); preparedStatements.put( nodeReferenceDeleteSQL, con.prepareStatement(nodeReferenceDeleteSQL)); if (!externalBLOBs) { preparedStatements.put(blobInsertSQL, con.prepareStatement(blobInsertSQL)); preparedStatements.put(blobUpdateSQL, con.prepareStatement(blobUpdateSQL)); preparedStatements.put(blobSelectSQL, con.prepareStatement(blobSelectSQL)); preparedStatements.put(blobSelectExistSQL, con.prepareStatement(blobSelectExistSQL)); preparedStatements.put(blobDeleteSQL, con.prepareStatement(blobDeleteSQL)); } } //--------------------------------------------------------< inner classes > static class SizedInputStream extends FilterInputStream { private final long size; private boolean consumed = false; SizedInputStream(InputStream in, long size) { super(in); this.size = size; } long getSize() { return size; } boolean isConsumed() { return consumed; } public int read() throws IOException { consumed = true; return super.read(); } public long skip(long n) throws IOException { consumed = true; return super.skip(n); } public int read(byte[] b) throws IOException { consumed = true; return super.read(b); } public int read(byte[] b, int off, int len) throws IOException { consumed = true; return super.read(b, off, len); } } class DbBLOBStore implements BLOBStore { /** * {@inheritDoc} */ public String createId(PropertyId id, int index) { // the blobId is a simple string concatenation of id plus index StringBuilder sb = new StringBuilder(); sb.append(id.toString()); sb.append('['); sb.append(index); sb.append(']'); return sb.toString(); } /** * {@inheritDoc} */ public InputStream get(String blobId) throws Exception { synchronized (blobSelectSQL) { Statement stmt = executeStmt(blobSelectSQL, new Object[]{blobId}); final ResultSet rs = stmt.getResultSet(); if (!rs.next()) { closeResultSet(rs); throw new Exception("no such BLOB: " + blobId); } InputStream in = rs.getBinaryStream(1); if (in == null) { // some databases treat zero-length values as NULL; // return empty InputStream in such a case closeResultSet(rs); return new ByteArrayInputStream(new byte[0]); } /** * return an InputStream wrapper in order to * close the ResultSet when the stream is closed */ return new FilterInputStream(in) { public void close() throws IOException { in.close(); // now it's safe to close ResultSet closeResultSet(rs); } }; } } /** * {@inheritDoc} */ public synchronized void put(String blobId, InputStream in, long size) throws Exception { Statement stmt = executeStmt(blobSelectExistSQL, new Object[]{blobId}); ResultSet rs = stmt.getResultSet(); // a BLOB exists if the result has at least one entry boolean exists = rs.next(); closeResultSet(rs); String sql = (exists) ? blobUpdateSQL : blobInsertSQL; executeStmt(sql, new Object[]{new SizedInputStream(in, size), blobId}); } /** * {@inheritDoc} */ public synchronized boolean remove(String blobId) throws Exception { Statement stmt = executeStmt(blobDeleteSQL, new Object[]{blobId}); return stmt.getUpdateCount() == 1; } } }