/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.jcr.value.binary; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Collections; import java.util.concurrent.TimeUnit; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import org.modeshape.common.annotation.ThreadSafe; import org.modeshape.common.util.StringUtil; import org.modeshape.jcr.JcrI18n; import org.modeshape.jcr.value.BinaryKey; import org.modeshape.jcr.value.BinaryValue; import com.zaxxer.hikari.HikariDataSource; /** * A {@link BinaryStore} implementation that uses a database for persisting binary values. * <p> * This binary store implementation establishes a connection to the specified database and then attempts to determine which type * of database is being used. ModeShape is aware of the following database types: * <ul> * <li><code>mysql</code></li> * <li><code>postgres</code></li> * <li><code>derby</code></li> * <li><code>hsql</code></li> * <li><code>h2</code></li> * <li><code>sqlite</code></li> * <li><code>db2</code></li> * <li><code>db2_390</code></li> * <li><code>informix</code></li> * <li><code>interbase</code></li> * <li><code>firebird</code></li> * <li><code>sqlserver</code></li> * <li><code>access</code></li> * <li><code>oracle</code></li> * <li><code>sybase</code></li> * </ul> * This binary store implementation then uses DDL and DML statements to create the table(s) if not already existing and to perform * the various operations required of a binary store. ModeShape can use database-specific statements, although a default set of * SQL-99 statements are used as a fallback. * </p> * <p> * These statements are read from a property file named "<code>binary_store_{type}_database.properties</code>", where where " * <code>{type}</code>" is one of the above-mentioned database type strings. These properties files are expected to be found on * the classpath directly under "org/modeshape/jcr/database". If the corresponding file is not found on the classpath, then the " * <code>binary_store_default_database.properties</code>" file provided by ModeShape is used. * </p> * <p> * ModeShape provides out-of-the-box database-specific files for several of the DBMSes that are popular within the open source * community. The properties files for the other database types are not provided (though the ModeShape community will gladly * incorporate them if you wish to make them available to us); in such cases, simply copy one of the provided properties files * (e.g., "<code>binary_store_default_database.properties</code>" is often a good start) and customize it for your particular * DBMS, naming it according to the pattern described above and including it on the classpath. * </p> * <p> * Note that this mechanism can also be used to override the statements that ModeShape does provide out-of-the-box. In such cases, * be sure to place the file on the classpath before the ModeShape JARs so that your file will be discovered first. * </p> * <p> * The JDBC driver used needs to be at least JDBC 1.4 (JDK 6) compliant, because * {@link PreparedStatement#setBinaryStream(int parameterIndex, java.io.InputStream x)} is being used. * </p> */ @ThreadSafe public class DatabaseBinaryStore extends AbstractBinaryStore { private static final boolean ALIVE = true; private static final boolean UNUSED = false; /** * JDBC params */ private final String driverClass; private final String connectionURL; private final String username; private final String password; private final String datasourceJNDILocation; private DataSource dataSource; /** * A temporary fs-based store which stores binaries before they are persisted in the DB */ private final FileSystemBinaryStore cache; /** * JDBC utility for working with the database. */ private Database database; /** * Create new store. * * @param driverClass JDBC driver class name * @param connectionURL database location * @param username database user name * @param password database password */ public DatabaseBinaryStore( String driverClass, String connectionURL, String username, String password ) { this.driverClass = driverClass; this.connectionURL = connectionURL; this.username = username; this.password = password; this.datasourceJNDILocation = null; this.cache = TransientBinaryStore.get(); } /** * Create new store that uses the JDBC DataSource in the given JNDI location. * * @param datasourceJNDILocation the JNDI name of the JDBC Data Source that should be used, or null */ public DatabaseBinaryStore( String datasourceJNDILocation ) { this.driverClass = null; this.connectionURL = null; this.username = null; this.password = null; this.datasourceJNDILocation = datasourceJNDILocation; this.cache = TransientBinaryStore.get(); } @Override public void start() { if (!StringUtil.isBlank(datasourceJNDILocation)) { lookupDataSource(); } else { initManagedDS(); } try (Connection connection = newConnection()) { this.database = new Database(connection); } catch (SQLException | IOException e) { throw new RuntimeException(e); } } @Override public BinaryValue storeValue( InputStream stream, final boolean markAsUnused ) throws BinaryStoreException { // store into temporary file system store and get SHA-1 final BinaryValue temp = cache.storeValue(stream, markAsUnused); // prepare new binary key based on SHA-1 final BinaryKey key = new BinaryKey(temp.getKey().toString()); try { return dbCall(connection -> { connection.setAutoCommit(false); if (database.contentExists(key, ALIVE, connection)) { return new StoredBinaryValue(DatabaseBinaryStore.this, key, temp.getSize()); } // check unused content if (database.contentExists(key, UNUSED, connection)) { if (!markAsUnused) { database.restoreContent(connection, Collections.singletonList(key)); } } else { try (InputStream is = temp.getStream()) { // store the content database.insertContent(key, is, temp.getSize(), connection); if (markAsUnused) { database.markUnused(Collections.singletonList(key), connection); } } } return new StoredBinaryValue(DatabaseBinaryStore.this, key, temp.getSize()); }); } catch (BinaryStoreException e) { if (e.getCause() instanceof SQLException) { // under certain conditions - e.g. in a cluster - someone else may have already inserted the binary // so try reading again return dbCall(connection -> { if (database.contentExists(key, !markAsUnused, connection)) { return new StoredBinaryValue(DatabaseBinaryStore.this, key, temp.getSize()); } // nothing there, so rethrow the original exception throw e; }); } throw e; } finally { // remove content from temp store cache.markAsUnused(temp.getKey()); } } /** * @inheritDoc * * In addition to the generic contract from {@link BinaryStore}, this implementation will use a database connection to read * the contents of a binary stream from the database. If such a stream cannot be found or an unexpected exception occurs, * the connection is always closed. * <p/> * However, if the content is found in the database, the {@link Connection} <b>is not closed</b> until the {@code InputStream} * is closed because otherwise actual streaming from the database could not be possible. */ @Override public InputStream getInputStream( BinaryKey key ) throws BinaryStoreException { Connection connection = newConnection(); try { InputStream inputStream = database.readContent(key, connection); if (inputStream == null) { // if we didn't find anything, the connection should've been closed already throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(key, database.getTableName())); } // the connection & statement will be left open until the stream is closed ! return inputStream; } catch (SQLException e) { throw new BinaryStoreException(e); } } @Override public void markAsUsed(final Iterable<BinaryKey> keys ) throws BinaryStoreException { dbCall(connection -> { connection.setAutoCommit(false); database.restoreContent(connection, keys); return null; }) ; } @Override public void markAsUnused( final Iterable<BinaryKey> keys ) throws BinaryStoreException { dbCall(connection -> { connection.setAutoCommit(false); database.markUnused(keys, connection); return null; }); } @Override public void removeValuesUnusedLongerThan( final long minimumAge, final TimeUnit unit ) throws BinaryStoreException { dbCall(connection -> { long deadline = System.currentTimeMillis() - unit.toMillis(minimumAge); database.removeExpiredContent(deadline, connection); return null; }); } @Override protected String getStoredMimeType( final BinaryValue source ) throws BinaryStoreException { return dbCall(connection -> { connection.setAutoCommit(false); BinaryKey key = source.getKey(); if (!database.contentExists(key, true, connection) && !database.contentExists(key, false, connection)) { throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(key, database.getTableName())); } return database.getMimeType(key, connection); }); } @Override protected void storeMimeType( final BinaryValue source, final String mimeType ) throws BinaryStoreException { dbCall(connection -> { database.setMimeType(source.getKey(), mimeType, connection); return null; }); } @Override public String getExtractedText( final BinaryValue source ) throws BinaryStoreException { return dbCall(connection -> { connection.setAutoCommit(false); BinaryKey key = source.getKey(); if (!database.contentExists(key, true, connection) && !database.contentExists(key, false, connection)) { throw new BinaryStoreException(JcrI18n.unableToFindBinaryValue.text(key, database.getTableName())); } return database.getExtractedText(key, connection); }); } @Override public void storeExtractedText( final BinaryValue source, final String extractedText ) throws BinaryStoreException { dbCall(connection -> { database.setExtractedText(source.getKey(), extractedText, connection); return null; }); } @Override public Iterable<BinaryKey> getAllBinaryKeys() throws BinaryStoreException { return dbCall(connection -> database.getBinaryKeys(connection)); } @Override public void shutdown() { if (dataSource instanceof HikariDataSource) { // we're managing this, so force a close to release all connections... ((HikariDataSource) this.dataSource).close(); } } private Connection newConnection() { try { return dataSource.getConnection(); } catch (SQLException e) { throw new RuntimeException(e); } } private void lookupDataSource() { try { InitialContext context = new InitialContext(); dataSource = (DataSource)context.lookup(datasourceJNDILocation); } catch (NamingException e) { throw new RuntimeException(e); } } private void initManagedDS() { logger.debug("Attempting to connect to '{0}' with '{1}' for username '{2}' and password '{3}'", connectionURL, driverClass, username, password); HikariDataSource hikariDS = new HikariDataSource(); hikariDS.setJdbcUrl(connectionURL); hikariDS.setDriverClassName(driverClass); hikariDS.setUsername(username); hikariDS.setPassword(password); this.dataSource = hikariDS; } @FunctionalInterface private interface DBCallable<T> { T execute( Connection connection ) throws Exception; } private <T> T dbCall( DBCallable<T> callable ) throws BinaryStoreException { try (Connection connection = newConnection()) { boolean autoCommit = true; try { connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); T result = callable.execute(connection); autoCommit = connection.getAutoCommit(); if (!autoCommit) { connection.commit(); } return result; } catch (Throwable t) { if (!autoCommit) { connection.rollback(); } throw t; } } catch (BinaryStoreException bse) { throw bse; } catch (Exception e) { throw new BinaryStoreException(e); } } }