/** * 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.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import org.apache.jena.jdbc.JdbcCompatibility; import org.apache.jena.jdbc.statements.DatasetPreparedStatement; import org.apache.jena.jdbc.statements.DatasetStatement; import org.apache.jena.jdbc.statements.JenaPreparedStatement; import org.apache.jena.jdbc.statements.JenaStatement; import org.apache.jena.query.Dataset ; import org.apache.jena.query.ReadWrite ; /** * Represents a connection to a {@link Dataset} instance * */ public abstract class DatasetConnection extends JenaConnection { protected Dataset ds; private boolean readonly = false; private ThreadLocal<ReadWrite> transactionType = new ThreadLocal<ReadWrite>(); private ThreadLocal<Integer> transactionParticipants = new ThreadLocal<Integer>(); /** * Creates a new dataset connection * * @param ds * Dataset * @param holdability * Cursor holdability * @param autoCommit * Sets auto-commit behavior 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 */ public DatasetConnection(Dataset ds, int holdability, boolean autoCommit, int transactionLevel, int compatibilityLevel) throws SQLException { super(holdability, autoCommit, transactionLevel, compatibilityLevel); this.ds = ds; } /** * Gets the dataset to which this connection pertains * * @return Dataset */ public final Dataset getJenaDataset() { return this.ds; } @Override protected void closeInternal() throws SQLException { try { if (this.ds != null) { ds.close(); ds = null; } } catch (Exception e) { throw new SQLException("Unexpected error closing a dataset backed connection", e); } finally { this.ds = null; } } @Override protected JenaStatement createStatementInternal(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { if (resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE) throw new SQLFeatureNotSupportedException("Dataset backed connections do not support scroll sensitive result sets"); if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) throw new SQLFeatureNotSupportedException("Dataset backed connections only supports read-only result sets"); return new DatasetStatement(this, resultSetType, ResultSet.FETCH_FORWARD, 0, resultSetHoldability, this.getAutoCommit(), this.getTransactionIsolation()); } @Override protected JenaPreparedStatement createPreparedStatementInternal(String sparql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { if (resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE) throw new SQLFeatureNotSupportedException("Dataset backed connections do not support scroll sensitive result sets"); if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) throw new SQLFeatureNotSupportedException("Dataset backed connections only supports read-only result sets"); return new DatasetPreparedStatement(sparql, this, resultSetType, ResultSet.FETCH_FORWARD, 0, resultSetHoldability, this.getAutoCommit(), this.getTransactionIsolation()); } @Override public boolean isClosed() { return this.ds == null; } @Override public boolean isReadOnly() { return this.readonly; } @Override public boolean isValid(int timeout) { return !this.isClosed(); } @Override public void setReadOnly(boolean readOnly) throws SQLException { if (this.isClosed()) throw new SQLException("Cannot set read-only mode on a closed connection"); this.readonly = readOnly; } @Override protected void checkTransactionIsolation(int level) throws SQLException { switch (level) { case TRANSACTION_NONE: return; case TRANSACTION_SERIALIZABLE: // Serializable is supported if the dataset supports transactions if (this.ds != null) if (this.ds.supportsTransactions()) return; // Otherwise we'll drop through and throw the error default: throw new SQLException(String.format("The Transaction level %d is not supported by this connection", level)); } } /** * Begins a new transaction * <p> * Transactions are typically thread scoped and are shared by each thread so * if there is an existing read transaction and another thread tries to * start a read transaction it will join the existing read transaction. * Trying to join a transaction not of the same type will produce an error. * </p> * * @param type * @throws SQLException */ public synchronized void begin(ReadWrite type) throws SQLException { try { if (this.isClosed()) throw new SQLException("Cannot start a transaction on a closed connection"); if (this.getTransactionIsolation() == Connection.TRANSACTION_NONE) throw new SQLException("Cannot start a transaction when transaction isolation is set to NONE"); if (ds.supportsTransactions()) { if (ds.isInTransaction()) { // Additional participant in existing transaction ReadWrite currType = this.transactionType.get(); if (currType.equals(type)) { this.transactionParticipants.set(this.transactionParticipants.get() + 1); } else { throw new SQLException( "Unable to start a transaction of a different type on the same thread as an existing transaction, please retry your operation on a different thread"); } } else { // Starting a new transaction this.transactionType.set(type); this.transactionParticipants.set(1); this.ds.begin(type); } } } catch (SQLException e) { throw e; } catch (Exception e) { throw new SQLException("Unexpected error starting a transaction", e); } } @Override protected synchronized void commitInternal() throws SQLException { try { if (ds.supportsTransactions()) { if (ds.isInTransaction()) { // How many participants are there in this transaction? int participants = this.transactionParticipants.get(); if (participants > 1) { // Transaction should remain active this.transactionParticipants.set(participants - 1); } else { // Now safe to commit ds.commit(); ds.end(); this.transactionParticipants.remove(); this.transactionType.remove(); } } else { throw new SQLException("Attempted to commit a transaction when there was no active transaction"); } } } catch (SQLException e) { throw e; } catch (Exception e) { throw new SQLException("Unexpected error committing the transaction", e); } } @Override protected synchronized void rollbackInternal() throws SQLException { try { if (ds.supportsTransactions()) { if (ds.isInTransaction()) { // Regardless of participants a rollback is always immediate ds.abort(); ds.end(); this.transactionType.remove(); this.transactionParticipants.remove(); } else { throw new SQLException("Attempted to rollback a transaction when there was no active transaction"); } } } catch (SQLException e) { throw e; } catch (Exception e) { throw new SQLException("Unexpected error rolling back the transaction", e); } } }