/** * 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.jena.jdbc.connections; import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLRecoverableException; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Savepoint; import java.sql.Statement; import java.sql.Struct; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.jena.graph.Triple ; import org.apache.jena.jdbc.JdbcCompatibility; import org.apache.jena.jdbc.metadata.JenaMetadata; import org.apache.jena.jdbc.postprocessing.ResultsPostProcessor; import org.apache.jena.jdbc.preprocessing.CommandPreProcessor; import org.apache.jena.jdbc.results.metadata.AskResultsMetadata; import org.apache.jena.jdbc.results.metadata.SelectResultsMetadata; import org.apache.jena.jdbc.results.metadata.TripleResultsMetadata; import org.apache.jena.jdbc.statements.JenaPreparedStatement; import org.apache.jena.jdbc.statements.JenaStatement; import org.apache.jena.query.Query ; import org.apache.jena.update.UpdateRequest ; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Abstract base implementation of a Jena JDBC connection * <p> * Generally speaking this is a faithful implementation of a JDBC connection but * it also provides a couple of Jena JDBC specific features: * </p> * <ol> * <li>JDBC compatibility level</li> * <li>Command pre-processors</li> * </ol> * <p> * The JDBC compatibility level allows the API to behave slightly differently * depending on how JDBC like you need it to be, see {@link JdbcCompatibility} * for more discussion on this. * </p> * <p> * Command pre-processors are an extension mechanism designed to allow Jena JDBC * connections to cope with the fact that the tools consuming the API may be * completely unaware that we speak SPARQL rather than SQL. They allow for * manipulation of incoming command text as well as manipulation of the parsed * SPARQL queries and updates as desired. * </p> */ public abstract class JenaConnection implements Connection { private static final Logger LOGGER = LoggerFactory.getLogger(JenaConnection.class); /** * Constant for default cursor holdability for Jena JDBC connections */ public static final int DEFAULT_HOLDABILITY = ResultSet.CLOSE_CURSORS_AT_COMMIT; /** * Constant for default auto-commit for Jena JDBC connections */ public final static boolean DEFAULT_AUTO_COMMIT = true; /** * Constant for default transaction isolation level for Jena JDBC * connections */ public final static int DEFAULT_ISOLATION_LEVEL = TRANSACTION_NONE; private Properties clientInfo = new Properties(); private int holdability = DEFAULT_HOLDABILITY; private SQLWarning warnings = null; private boolean autoCommit = DEFAULT_AUTO_COMMIT; private int isolationLevel = DEFAULT_ISOLATION_LEVEL; private int compatibilityLevel = JdbcCompatibility.DEFAULT; private List<Statement> statements = new ArrayList<Statement>(); private List<CommandPreProcessor> preProcessors = new ArrayList<CommandPreProcessor>(); private List<ResultsPostProcessor> postProcessors = new ArrayList<ResultsPostProcessor>(); /** * Creates a new connection * * @param holdability * Cursor holdability * @param autoCommit * Sets auto-commit behaviour for the connection * @param transactionLevel * Sets transaction isolation level for the connection * @param compatibilityLevel * Sets JDBC compatibility level for the connection, see * {@link JdbcCompatibility} * @throws SQLException * Thrown if the arguments are invalid */ public JenaConnection(int holdability, boolean autoCommit, int transactionLevel, int compatibilityLevel) throws SQLException { this.checkHoldability(holdability); this.holdability = holdability; this.setAutoCommit(autoCommit); this.setTransactionIsolation(transactionLevel); this.compatibilityLevel = JdbcCompatibility.normalizeLevel(compatibilityLevel); } /** * Gets the JDBC compatibility level that is in use, see * {@link JdbcCompatibility} for explanations * * @return Compatibility level */ public int getJdbcCompatibilityLevel() { return this.compatibilityLevel; } /** * Sets the JDBC compatibility level that is in use, see * {@link JdbcCompatibility} for explanations. * <p> * Changing the level may not effect existing open objects, behaviour in * this case will be implementation specific. * </p> * * @param level * Compatibility level */ public void setJdbcCompatibilityLevel(int level) { this.compatibilityLevel = JdbcCompatibility.normalizeLevel(level); } /** * Adds a command pre-processor to the connection * * @param preProcessor * Pre-processor to add */ public final void addPreProcessor(CommandPreProcessor preProcessor) { if (preProcessor == null) return; this.preProcessors.add(preProcessor); } /** * Adds a results post-processor to the connection * * @param postProcessor * Post-processor to add */ public final void addPostProcessor(ResultsPostProcessor postProcessor) { if (postProcessor == null) return; this.postProcessors.add(postProcessor); } /** * Inserts a command pre-processor for the connection * * @param index * Index to insert at * @param preProcessor * Pre-processor */ public final void insertPreProcessor(int index, CommandPreProcessor preProcessor) { if (preProcessor == null) return; this.preProcessors.add(index, preProcessor); } /** * Inserts a results post-processor for the connection * * @param index * Index to insert at * @param postProcessor * Post-processor */ public final void insertPostProcessor(int index, ResultsPostProcessor postProcessor) { if (postProcessor == null) return; this.postProcessors.add(index, postProcessor); } /** * Removes a command pre-processor from the connection * * @param preProcessor * Pre-processor to remove */ public final void removePreProcessor(CommandPreProcessor preProcessor) { if (preProcessor == null) return; this.preProcessors.remove(preProcessor); } /** * Removes a results post-processor from the connection * * @param postProcessor * Post-processor to remove */ public final void removePostProcessor(ResultsPostProcessor postProcessor) { if (postProcessor == null) return; this.postProcessors.remove(postProcessor); } /** * Removes a command pre-processor from the connection * * @param index * Index to remove at */ public final void removePreProcessor(int index) { this.preProcessors.remove(index); } /** * Removes a results post-processor from the connection * * @param index * Index to remove at */ public final void removePostProcessor(int index) { this.postProcessors.remove(index); } /** * Clears all command pre-processor from the connection */ public final void clearPreProcessors() { this.preProcessors.clear(); } /** * Clears all command post-processors from the connection */ public final void clearPostProcessors() { this.postProcessors.clear(); } /** * Gets the currently registered pre-processors for the connection * * @return Iterator of pre-processors */ public final Iterator<CommandPreProcessor> getPreProcessors() { return this.preProcessors.iterator(); } /** * Gets the currently registered post-processors for the connection * * @return Iterator of post-processors */ public final Iterator<ResultsPostProcessor> getPostProcessors() { return this.postProcessors.iterator(); } /** * Apply registered pre-processors to the given command text * * @param text * Command Text * @return Command Text after processing by registered pre-processors * @throws SQLException */ public final String applyPreProcessors(String text) throws SQLException { for (CommandPreProcessor preProcessor : this.preProcessors) { if (preProcessor == null) continue; text = preProcessor.preProcessCommandText(text); } return text; } /** * Applies registered pre-processors to the given query * * @param q * Query * @return Query after processing by registered pre-processors * @throws SQLException */ public final Query applyPreProcessors(Query q) throws SQLException { for (CommandPreProcessor preProcessor : this.preProcessors) { if (preProcessor == null) continue; q = preProcessor.preProcessQuery(q); } return q; } /** * Applies registered pre-processors to the given update * * @param u * Update * @return Update after processing by registered pre-processors * @throws SQLException */ public final UpdateRequest applyPreProcessors(UpdateRequest u) throws SQLException { for (CommandPreProcessor preProcessor : this.preProcessors) { if (preProcessor == null) continue; u = preProcessor.preProcessUpdate(u); } return u; } /** * Applies registered post-processors to the given results * * @param results * Results * @return Results after processing by registered post-processors * @throws SQLException */ public final org.apache.jena.query.ResultSet applyPostProcessors(org.apache.jena.query.ResultSet results) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; results = postProcessor.postProcessResults(results); } return results; } /** * Applies registered post-processors to the given results * * @param triples * Results * @return Results after processing by registered post-processors * @throws SQLException */ public final Iterator<Triple> applyPostProcessors(Iterator<Triple> triples) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; triples = postProcessor.postProcessResults(triples); } return triples; } /** * Applies registered post-processors to the given results * * @param result * Result * @return Result after processing by registered post-processors * @throws SQLException */ public final boolean applyPostProcessors(boolean result) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; result = postProcessor.postProcessResults(result); } return result; } /** * Applies registered post-processors to the given results metadata * * @param metadata * Results metadata * @return Results metadata after processing by registered post-processors * @throws SQLException */ public final SelectResultsMetadata applyPostProcessors(SelectResultsMetadata metadata) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; metadata = postProcessor.postProcessResultsMetadata(metadata); } return metadata; } /** * Applies registered post-processors to the given results metadata * * @param metadata * Results metadata * @return Results metadata after processing by registered post-processors * @throws SQLException */ public final TripleResultsMetadata applyPostProcessors(TripleResultsMetadata metadata) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; metadata = postProcessor.postProcessResultsMetadata(metadata); } return metadata; } /** * Applies registered post-processors to the given results metadata * * @param metadata * Results metadata * @return Results metadata after processing by registered post-processors * @throws SQLException */ public final AskResultsMetadata applyPostProcessors(AskResultsMetadata metadata) throws SQLException { for (ResultsPostProcessor postProcessor : this.postProcessors) { if (postProcessor == null) continue; metadata = postProcessor.postProcessResultsMetadata(metadata); } return metadata; } @Override public boolean isWrapperFor(Class<?> arg0) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public <T> T unwrap(Class<T> arg0) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void clearWarnings() { this.warnings = null; } @Override public final void close() throws SQLException { try { LOGGER.info("Closing connection..."); // Close any open statements this.closeStatements(); } finally { this.closeInternal(); LOGGER.info("Connection was closed"); } } private void closeStatements() throws SQLException { synchronized (this.statements) { if (this.statements.size() > 0) { LOGGER.info("Attempting to close " + this.statements.size() + " open statements"); for (Statement stmt : this.statements) { stmt.close(); } LOGGER.info("All open statements were closed"); this.statements.clear(); } } } protected abstract void closeInternal() throws SQLException; @Override public void commit() throws SQLException { if (this.isClosed()) throw new SQLException("Cannot commit on a closed connection"); try { LOGGER.info("Attempting to commit a transaction..."); // Get the implementation to do the actual commit this.commitInternal(); LOGGER.info("Transaction was committed"); // If applicable close cursors if (this.holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT) { LOGGER.info("Holdability set to CLOSE_CURSORS_AT_COMMIT so closing open statements"); this.closeStatements(); } } catch (SQLException e) { // Throw as-is throw e; } catch (Exception e) { // Wrap as SQLException LOGGER.error("Unexpected error in transaction commit", e); throw new SQLException("Unexpected error committing transaction", e); } } protected abstract void commitInternal() throws SQLException; @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public Blob createBlob() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public Clob createClob() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public NClob createNClob() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public SQLXML createSQLXML() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public final Statement createStatement() throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } @Override public final Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.createStatement(resultSetType, resultSetConcurrency, this.getHoldability()); } @Override public final Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); JenaStatement stmt = this.createStatementInternal(resultSetType, resultSetConcurrency, resultSetHoldability); synchronized (this.statements) { this.statements.add(stmt); } return stmt; } protected abstract JenaStatement createStatementInternal(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException; @Override public Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public boolean getAutoCommit() { return this.autoCommit; } @Override public String getCatalog() { return JenaMetadata.DEFAULT_CATALOG; } @Override public Properties getClientInfo() { return this.clientInfo; } @Override public String getClientInfo(String name) { return this.clientInfo.getProperty(name); } @Override public int getHoldability() { return this.holdability; } @Override public abstract DatabaseMetaData getMetaData() throws SQLException; @Override public int getTransactionIsolation() { return this.isolationLevel; } @Override public Map<String, Class<?>> getTypeMap() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public SQLWarning getWarnings() { return this.warnings; } @Override public abstract boolean isClosed() throws SQLException; @Override public abstract boolean isReadOnly() throws SQLException; @Override public abstract boolean isValid(int timeout) throws SQLException; @Override public String nativeSQL(String sql) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public CallableStatement prepareCall(String sql) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.prepareStatement(sql, resultSetType, resultSetConcurrency, this.holdability); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot create a statement after the connection was closed"); return this.createPreparedStatementInternal(sql, resultSetType, resultSetConcurrency, resultSetHoldability); } /** * Helper method which derived implementations must implement to provide an * actual prepared statement implementation * * @param sql * SPARQL command * @param resultSetType * Desired result set type * @param resultSetConcurrency * Result set concurrency * @param resultSetHoldability * Result set holdability * @return Prepared statement * @throws SQLException */ protected abstract JenaPreparedStatement createPreparedStatementInternal(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException; @Override public void releaseSavepoint(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void rollback() throws SQLException { if (this.isClosed()) throw new SQLException("Cannot rollback on a closed connection"); try { LOGGER.info("Attempting to rollback a transaction..."); // Get the implementation to do the actual rollback this.rollbackInternal(); LOGGER.info("Transaction was rolled back"); // Close any open statements if applicable if (this.holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT) { LOGGER.info("Holdability is set to CLOSE_CURSORS_AT_COMMIT so closing open statements"); this.closeStatements(); } } catch (SQLException e) { // Throw as-is throw e; } catch (Exception e) { throw new SQLException("Unexpected error rolling back transaction", e); } } protected abstract void rollbackInternal() throws SQLException; @Override public void rollback(Savepoint savepoint) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void setAutoCommit(boolean autoCommit) { this.autoCommit = autoCommit; } @Override public void setCatalog(String catalog) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void setClientInfo(Properties properties) { this.clientInfo = properties; } @Override public void setClientInfo(String name, String value) { this.clientInfo.put(name, value); } @Override public void setHoldability(int holdability) throws SQLException { this.checkHoldability(holdability); this.holdability = holdability; } /** * Helper method that checks whether the given holdability setting is valid, * throws an error if it is not * * @param h * Holdability Setting * @throws SQLException * Thrown if the setting is not valid */ protected void checkHoldability(int h) throws SQLException { switch (h) { case ResultSet.CLOSE_CURSORS_AT_COMMIT: case ResultSet.HOLD_CURSORS_OVER_COMMIT: return; default: throw new SQLRecoverableException(String.format("%d is not a valid holdability setting", h)); } } @Override public abstract void setReadOnly(boolean readOnly) throws SQLException; @Override public Savepoint setSavepoint() throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public Savepoint setSavepoint(String name) throws SQLException { throw new SQLFeatureNotSupportedException(); } @Override public void setTransactionIsolation(int level) throws SQLException { this.checkTransactionIsolation(level); this.isolationLevel = level; } /** * Helper method which checks that a transaction isolation level is valid, * should throw an exception if the given level is not valid for the * connection * * @param level * Isolation Level * @throws SQLException * Thrown if the isolation level is not valid */ protected abstract void checkTransactionIsolation(int level) throws SQLException; @Override public void setTypeMap(Map<String, Class<?>> map) throws SQLException { throw new SQLFeatureNotSupportedException(); } /** * Helper method that derived classes may use to set warnings * * @param warning * Warning */ protected void setWarning(SQLWarning warning) { LOGGER.warn("SQL Warning was issued", warning); if (this.warnings == null) { this.warnings = warning; } else { // Chain with existing warnings warning.setNextWarning(this.warnings); this.warnings = warning; } } /** * Helper method that derived classes may use to set warnings * * @param warning * Warning */ protected void setWarning(String warning) { this.setWarning(new SQLWarning(warning)); } /** * Helper method that derived classes may use to set warnings * * @param warning * Warning * @param cause * Cause */ protected void setWarning(String warning, Throwable cause) { this.setWarning(new SQLWarning(warning, cause)); } //--- Java6/7 compatibility. public void setSchema(String schema) throws SQLException { throw new SQLFeatureNotSupportedException(); } public String getSchema() throws SQLException { throw new SQLFeatureNotSupportedException(); } public void abort(java.util.concurrent.Executor executor) throws SQLException { throw new SQLFeatureNotSupportedException(); } public int getNetworkTimeout() throws SQLException { throw new SQLFeatureNotSupportedException(); } public void setNetworkTimeout(java.util.concurrent.Executor executor, int milliseconds) throws SQLException { throw new SQLFeatureNotSupportedException(); } }