/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
*/
package org.geotools.arcsde.data;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.arcsde.session.Command;
import org.geotools.arcsde.session.ISession;
import org.geotools.arcsde.versioning.ArcSdeVersionHandler;
import org.geotools.arcsde.versioning.TransactionVersionHandler;
import org.geotools.data.FeatureListenerManager;
import org.geotools.data.Transaction;
import org.geotools.util.logging.Logging;
import com.esri.sde.sdk.client.SeConnection;
import com.esri.sde.sdk.client.SeException;
import com.esri.sde.sdk.client.SeObjectId;
import com.esri.sde.sdk.client.SeState;
import com.esri.sde.sdk.client.SeVersion;
/**
* Store the transaction state for <code>ArcSDEFeatureWriter</code> instances.
*
* @author Jake Fear
* @author Gabriel Roldan
* @source $URL:
* http://svn.geotools.org/geotools/trunk/gt/modules/plugin/arcsde/datastore/src/main/java
* /org/geotools/arcsde/data/ArcTransactionState.java $
* @version $Id$
*/
final class ArcTransactionState implements Transaction.State {
private static final Logger LOGGER = Logging.getLogger(ArcTransactionState.class.getName());
/**
* ArcSDEDataStore we can use to look up a Session for our Transaction.
* <p>
* The ConnectionPool will hold this connection open for us until commit(), rollback() or
* close() is called.
*/
private ArcSDEDataStore dataStore;
private Transaction transaction;
private final FeatureListenerManager listenerManager;
/**
* Set of typename changed to fire changed events for at commit and rollback.
*/
private final Set<String> typesChanged = new HashSet<String>();
public SeState currentVersionState;
public SeObjectId initialStateId;
public SeVersion defaultVersion;
private ArcSdeVersionHandler versionHandler = ArcSdeVersionHandler.NONVERSIONED_HANDLER;
/**
* Creates a new ArcTransactionState object.
*
* @param listenerManager
* @param arcSDEDataStore
* connection pool where to grab a connection and hold it while there's a transaction
* open (signaled by any use of {@link #getConnection()}
*/
ArcTransactionState(ArcSDEDataStore dataStore, final FeatureListenerManager listenerManager) {
this.dataStore = dataStore;
this.listenerManager = listenerManager;
}
private void setupVersioningHandling(final String versionName) throws IOException {
// create a versioned handler only if not already settled up, as this method
// may be called for each layer inside a transaction
if (versionHandler == ArcSdeVersionHandler.NONVERSIONED_HANDLER) {
ISession session = getConnection();
versionHandler = new TransactionVersionHandler(session, versionName);
}
}
/**
* @param versioName
* the name of the version to work against
* @return
* @throws IOException
*/
public ArcSdeVersionHandler getVersionHandler(final boolean ftIsVersioned,
final String versionName) throws IOException {
if (ftIsVersioned) {
setupVersioningHandling(versionName);
}
return versionHandler;
}
/**
* Registers a feature change event over a feature type.
* <p>
* To be called by {@link TransactionFeatureWriter#write()} so this state can fire a changed
* event at {@link #commit()} and {@link #rollback()}.
* </p>
*
* @param typeName
* the type name of the feature changed (inserted/removed/modified).
*/
public void addChange(final String typeName) {
typesChanged.add(typeName);
}
/**
* Commits the transaction and returns the connection to the pool. A new one will be grabbed
* when needed.
* <p>
* Preconditions:
* <ul>
* <li>{@link #setTransaction(Transaction)} already called with non <code>null</code> argument.
* <li>
* </ul>
* </p>
*/
public void commit() throws IOException {
failIfClosed();
final ISession session = this.getConnection();
final Command<Void> commitCommand = new Command<Void>() {
@Override
public Void execute(ISession session, SeConnection connection) throws SeException,
IOException {
try {
if (currentVersionState != null) {
SeObjectId parentStateId = initialStateId;
// Change the version's state pointer to the last edit state.
defaultVersion.changeState(currentVersionState.getId());
// Trim the state tree.
currentVersionState.trimTree(parentStateId, currentVersionState.getId());
}
} catch (SeException se) {
LOGGER.log(Level.WARNING, se.getMessage(), se);
close();
throw se;
}
return null;
}
};
try {
session.issue(commitCommand);
fireChanges(true);
versionHandler.commitEditState();
} catch (IOException e) {
versionHandler.rollbackEditState();
throw e;
}
}
/**
*
*/
public void rollback() throws IOException {
failIfClosed();
try {
versionHandler.rollbackEditState();
// fire changes in the calling thread
fireChanges(false);
} catch (IOException se) {
// release resources
close();
LOGGER.log(Level.WARNING, se.getMessage(), se);
throw se;
}
}
/**
* Fires the per typename changes registered through {@link #addChange(String)} and clears the
* changes cache.
*/
private void fireChanges(final boolean commit) {
for (String typeName : typesChanged) {
listenerManager.fireChanged(typeName, transaction, commit);
}
typesChanged.clear();
}
/**
*
*/
public void addAuthorization(String authId) {
// intentionally blank
}
/**
* @see Transaction.State#setTransaction(Transaction)
* @param transaction
* transaction information, <code>null</code> signals this state lifecycle end.
* @throws IllegalStateException
* if close() is called while a transaction is in progress
*/
public void setTransaction(final Transaction transaction) {
if (Transaction.AUTO_COMMIT.equals(transaction)) {
throw new IllegalArgumentException("Cannot use Transaction.AUTO_COMMIT here");
}
if (transaction == null) {
// this is a call to free resources (ugly, but that's what the API
// says)
close();
} else if (this.transaction != null) {
// assert this assumption
throw new IllegalStateException(
"Once a transaction is set, it is "
+ "illegal to call Transaction.State.setTransaction with anything other than null: "
+ transaction);
}
this.transaction = transaction;
}
/**
* If this state has been closed throws an unchecked exception as its clearly a broken workflow.
*
* @throws IllegalStateException
* if the transaction state has been closed.
*/
private void failIfClosed() throws IllegalStateException {
if (dataStore == null) {
throw new IllegalStateException("This transaction state has already been closed");
}
}
/**
* Releases resources and invalidates this state (signaled by setting the connection to null)
*/
private void close() {
if (dataStore == null) {
return;
}
dataStore = null;
}
/**
* Used only within the package to provide access to a single connection on which this
* transaction is being conducted.
*
* @return connection
* @throws IOException
*/
ISession getConnection() throws IOException {
failIfClosed();
// the pool is keeping track of connection according to transaction for us
return dataStore.getSession(transaction);
}
public Transaction getTransaction() {
return transaction;
}
}