/* * 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.session; import java.io.IOException; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.WeakHashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.geotools.arcsde.ArcSdeException; import com.esri.sde.sdk.client.SeColumnDefinition; import com.esri.sde.sdk.client.SeConnection; import com.esri.sde.sdk.client.SeDBMSInfo; import com.esri.sde.sdk.client.SeDelete; import com.esri.sde.sdk.client.SeException; import com.esri.sde.sdk.client.SeInsert; import com.esri.sde.sdk.client.SeLayer; import com.esri.sde.sdk.client.SeObjectId; import com.esri.sde.sdk.client.SeQuery; import com.esri.sde.sdk.client.SeRasterColumn; import com.esri.sde.sdk.client.SeRegistration; import com.esri.sde.sdk.client.SeRelease; import com.esri.sde.sdk.client.SeRow; import com.esri.sde.sdk.client.SeSqlConstruct; import com.esri.sde.sdk.client.SeState; import com.esri.sde.sdk.client.SeStreamOp; import com.esri.sde.sdk.client.SeTable; import com.esri.sde.sdk.client.SeUpdate; import com.esri.sde.sdk.geom.GeometryFactory; /** * Provides thread safe access to an SeConnection. * <p> * This class has become more and more magic over time! It no longer represents a Connection but * provides "safe" access to a connection. * <p> * * @author Gabriel Roldan * @author Jody Garnett * @version $Id$ * @since 2.3.x */ class Session implements ISession { public static final Logger LOGGER; static { /* * This Jar may be used withoug geotools' gt-metadata being in the class path, so try to use * the org.geotools.util.logging.Logging.getLogger method reflectively and fall back to * plain java.util.logger if that's the case */ Logger logger = null; try { Class<?> clazz = Class.forName("org.geotools.util.logging.Logging"); Method method = clazz.getMethod("getLogger", String.class); logger = (Logger) method.invoke(null, "org.geotools.arcsde.session"); } catch (Exception e) { logger = Logger.getLogger("org.geotools.arcsde.session"); logger.info("org.geotools.util.logging.Logging seems not to be in the classpath, " + "acquired Logger through java.util.Logger"); } LOGGER = logger; } /** * How many seconds must have elapsed since the last connection round trip to the server for * {@link #testServer()} to actually check the connection's validity */ protected static final long TEST_SERVER_ROUNDTRIP_INTERVAL_SECONDS = 5; /** Actual SeConnection being protected */ private final SeConnection connection; /** * SessionPool used to manage open connections (shared). */ private final SessionPool pool; private final ArcSDEConnectionConfig config; /** * Used to assign unique ids to each new session */ private static final AtomicInteger sessionCounter = new AtomicInteger(); /** * Global unique id for this session */ private final int sessionId; private boolean transactionInProgress; private boolean isPassivated; private Map<String, SeTable> cachedTables = new WeakHashMap<String, SeTable>(); private Map<String, SeLayer> cachedLayers = new WeakHashMap<String, SeLayer>(); /** * The SeConnection bound task executor, ensures all operations against a given connection are * performed in the same thread regardless of the thread the {@link #issue(Command)} is being * called from. */ //private final ExecutorService taskExecutor; /** * Thread used by the taskExecutor; so we can detect recursion. */ private Thread commandThread; /** * Keeps track of the number of references to this session (ie, how many times it has been * {@link #markActive() activated} so it's only actually {@link #dispose() disposed} when the * reference count gets down to zero. */ private final AtomicInteger referenceCounter = new AtomicInteger(); /** * Executes a {@link Command} inside the Session's worker thread */ private final class SessionTask<T> implements Callable<T> { private final Command<T> command; private SessionTask(Command<T> command) { this.command = command; } /** * Executes a {@link Command} inside the Session's worker thread * * @see java.util.concurrent.Callable#call() * @see Session#issue(Command) */ public T call() throws Exception { final Thread currentThread = Thread.currentThread(); if (commandThread != currentThread) { LOGGER.fine("updating command thread from " + commandThread + " to " + currentThread); commandThread = currentThread; } if (currentThread != commandThread) { throw new IllegalStateException("currentThread != commandThread"); } try { return command.execute(Session.this, connection); } catch (Exception e) { if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.log(Level.FINEST, "Command execution failed for Session " + Session.this.sessionId + " in thread " + currentThread.getId(), e); } else if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Command execution failed for Session " + Session.this.sessionId + " in thread " + currentThread.getId()); } if (e instanceof SeException) { throw new ArcSdeException((SeException) e); } else if (e instanceof IOException) { throw e; } throw new RuntimeException("Command execution failed for Session " + Session.this.sessionId + " in thread " + currentThread.getId(), e); } } } /** * A custom {@link ThreadFactory} for the Session's {@link ExecutorService} for the sole reason * of giving threads a significative name (unvaluable when debugging/profiling) */ private static class SessionThreadFactory implements ThreadFactory { private final int sessionId; private static final ThreadGroup group = new ThreadGroup("ArcSDE Session threads"); public SessionThreadFactory(final int sessionId) { this.sessionId = sessionId; } /** * @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable) */ public Thread newThread(final Runnable r) { Thread t = new Thread(group, r, "ArcSDE Session " + sessionId); t.setDaemon(true); return t; } } /** * Provides safe access to an SeConnection. * * @param pool * SessionPool used to manage SeConnection * @param config * Used to set up a SeConnection * @throws SeException * If we cannot connect */ Session(final SessionPool pool, final ArcSDEConnectionConfig config) throws IOException { this.sessionId = sessionCounter.incrementAndGet(); this.config = config; this.pool = pool; //this.taskExecutor = Executors.newSingleThreadExecutor(new SessionThreadFactory(sessionId)); // grab command thread, held by taskExecutor updateCommandThread(); /* * This ensures the connection runs always on the same thread. Will fail if its accessed by * different threads */ final CreateSeConnectionCommand connectionCommand; connectionCommand = new CreateSeConnectionCommand(config, sessionId); try { this.connection = issue(connectionCommand); } catch (IOException e) { // make sure a connection creation failure does not leave a stale thread //this.taskExecutor.shutdownNow(); throw e; } catch (RuntimeException shouldntHappen) { //this.taskExecutor.shutdownNow(); throw shouldntHappen; } } /** * @see ISession#issue(org.geotools.arcsde.session.Command) */ public synchronized <T> T issue(final Command<T> command) throws IOException { try { if (connection == null) { return command.execute(this, null); } else { return command.execute(this, connection); } } catch (SeException e) { throw new ArcSdeException(e); } // final Thread callingThread = Thread.currentThread(); // if (callingThread == commandThread) { // // Called command inside command // try { // return command.execute(this, connection); // } catch (SeException e) { // Throwable cause = e.getCause(); // if (cause instanceof IOException) { // throw (IOException) cause; // } // throw new ArcSdeException(e); // } // } else { // final SessionTask<T> sessionTask = new SessionTask<T>(command); // final Future<T> task = taskExecutor.submit(sessionTask); // T result; // try { // result = task.get(); // } catch (InterruptedException e) { // updateCommandThread(); // throw new RuntimeException("Command execution abruptly interrupted", e); // } catch (ExecutionException e) { // updateCommandThread(); // Throwable cause = e.getCause(); // if (cause instanceof IOException) { // throw (IOException) cause; // } else if (cause instanceof SeException) { // throw new ArcSdeException((SeException) cause); // } // throw (IOException) new IOException().initCause(cause); // } // return result; // } } private void updateCommandThread() { final Callable<Object> task = new Callable<Object>() { public Object call() throws Exception { final Thread currentThread = Thread.currentThread(); if (currentThread != commandThread) { LOGGER.fine("updating command thread from " + commandThread + " to " + currentThread); commandThread = currentThread; } return null; } }; // used to detect when thread has been // restarted after error // and block until task is executed // try { // taskExecutor.submit(task).get(); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } catch (ExecutionException e) { // throw new RuntimeException(e); // } } /** * @see ISession#testServer() */ public final void testServer() throws IOException { /* * This method is called often (every time a session is to be returned from the pool) to * check if it's still valid. We can call getTimeSinceLastRT safely since it does not * require a server roundtrip and hence there's no risk of violating thread safety. So we do * it before issuing the command to avoid the perf penalty imposed by running the command if * not needed. */ final long secondsSinceLastServerRoundTrip = this.connection.getTimeSinceLastRT(); if (TEST_SERVER_ROUNDTRIP_INTERVAL_SECONDS < secondsSinceLastServerRoundTrip) { issue(Commands.TEST_SERVER_COMMAND); } } /** * @see ISession#isClosed() */ public final boolean isClosed() { return this.connection.isClosed(); } /** * Marks the connection as being active (i.e. its out of the pool and ready to be used). * <p> * Shall be called just before being returned from the connection pool * </p> * * @see #isPassivated * @see #checkActive() */ void markActive() { referenceCounter.incrementAndGet(); this.isPassivated = false; } /** * Marks the connection as being inactive (i.e. laying on the connection pool) * <p> * Shall be callled just before sending it back to the pool * </p> * * @see #markActive() * @see #isPassivated * @see #checkActive() */ void markInactive() { if (referenceCounter.get() != 0) { throw new IllegalStateException("referenceCount = " + referenceCounter); } this.isPassivated = true; } /** * @see ISession#isPassivated() */ public boolean isDisposed() { return isPassivated; } /** * Sanity check method called before every public operation delegates to the superclass. * * @throws IllegalStateException * if {@link #isDisposed() isPassivated() == true} as this is a serious workflow * breackage. */ private void checkActive() { if (isDisposed()) { throw new IllegalStateException("Unrecoverable error: " + toString() + " is passivated, shall not be used!"); } } /** * @see ISession#getLayer(java.lang.String) */ public SeLayer getLayer(final String layerName) throws IOException { checkActive(); if (!cachedLayers.containsKey(layerName)) { issue(new Command<Void>() { @Override public Void execute(final ISession session, final SeConnection connection) throws SeException, IOException { synchronized (cachedLayers) { if (!cachedLayers.containsKey(layerName)) { SeTable table = getTable(layerName); String shapeColumn = getShapeColumn(table); if (shapeColumn == null) { return null; } SeLayer layer = new SeLayer(connection, layerName, shapeColumn); cachedLayers.put(layerName, layer); } } return null; } }); } SeLayer seLayer = cachedLayers.get(layerName); if (seLayer == null) { throw new NoSuchElementException("Layer '" + layerName + "' not found"); } return seLayer; } private String getShapeColumn(SeTable table) throws ArcSdeException { try { for (SeColumnDefinition aDef : table.describe()) { if (aDef.getType() == SeColumnDefinition.TYPE_SHAPE) { return aDef.getName(); } } } catch (SeException e) { throw new ArcSdeException("Exception describing table " + table.getName(), e); } return null; } /** * @see ISession#getRasterColumn(java.lang.String) */ public synchronized SeRasterColumn getRasterColumn(final String rasterName) throws IOException { throw new UnsupportedOperationException("Waiting for a proper implementation"); } /** * @see org.geotools.arcsde.session.ISession#getRasterColumns() */ public List<String> getRasterColumns() throws IOException { checkActive(); List<String> rasterNames = issue(Commands.GetRasterColumnNamesCommand); return rasterNames; } /** * @see ISession#getTable(java.lang.String) */ public SeTable getTable(final String tableName) throws IOException { checkActive(); if (!cachedTables.containsKey(tableName)) { issue(new Command<Void>() { @Override public Void execute(final ISession session, final SeConnection connection) throws SeException, IOException { synchronized (cachedTables) { if (!cachedTables.containsKey(tableName)) { SeTable table = new SeTable(connection, tableName); try { table.describe(); } catch (SeException e) { throw new NoSuchElementException("Table '" + tableName + "' not found"); } cachedTables.put(tableName, table); } } return null; } }); } SeTable seTable = (SeTable) cachedTables.get(tableName); if (seTable == null) { throw new NoSuchElementException("Table '" + tableName + "' not found"); } return seTable; } /** * @see ISession#startTransaction() */ public void startTransaction() throws IOException { checkActive(); issue(Commands.StartTransactionCommand); transactionInProgress = true; } /** * @see ISession#commitTransaction() */ public void commitTransaction() throws IOException { checkActive(); issue(Commands.CommitTransactionCommand); transactionInProgress = false; } /** * @see ISession#isTransactionActive() */ public boolean isTransactionActive() { checkActive(); return transactionInProgress; } /** * @see ISession#rollbackTransaction() */ public void rollbackTransaction() throws IOException { checkActive(); try { issue(Commands.RollbackTransactionCommand); } finally { transactionInProgress = false; } } /** * @see ISession#dispose() */ public void dispose() throws IllegalStateException { checkActive(); final int refCount = referenceCounter.decrementAndGet(); if (refCount > 0) { // ignore if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest("---------> Ignoring disposal, ref count is still " + refCount + " for " + this); } // System.err.println("---------> Ignoring disposal, ref count is still " + refCount // + " for " + this); return; } if (LOGGER.isLoggable(Level.FINEST)) { LOGGER.finest(" -> RefCount is " + refCount + ". Disposing " + this); } if (transactionInProgress) { throw new IllegalStateException( "Transaction is in progress, should commit or rollback before closing"); } try { // System.err.println("---------> Disposing " + this + " on thread " + // Thread.currentThread().getName()); this.pool.returnObject(this); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } @Override public String toString() { return "Session[" + sessionId + "]"; } /** * Actually closes the connection, called when the session is discarded from the pool */ void destroy() { LOGGER.fine("Destroying connection " + toString()); try { issue(Commands.CloseConnectionCommand); } catch (Exception e) { LOGGER.log(Level.FINE, "closing connection " + toString(), e); } finally { //taskExecutor.shutdown(); } } /** * @see ISession#equals(java.lang.Object) */ @Override public boolean equals(Object other) { return other == this; } /** * @see ISession#hashCode() */ @Override public int hashCode() { return 17 ^ this.config.hashCode(); } /** * @see ISession#getLayers() */ public List<SeLayer> getLayers() throws IOException { return issue(Commands.GetLayersCommand); } /** * @see ISession#getUser() */ public String getUser() throws IOException { return issue(Commands.GetUserCommand); } /** * @see ISession#getRelease() */ public SeRelease getRelease() throws IOException { return issue(Commands.GetReleaseCommand); } /** * @see ISession#getDatabaseName() */ public String getDatabaseName() throws IOException { return issue(Commands.GetDatabaseNameCommand); } /** * @see ISession#getDBMSInfo() */ public SeDBMSInfo getDBMSInfo() throws IOException { return issue(Commands.getDBMSInfoCommand); } /** * @see ISession#createSeRegistration(java.lang.String) */ public SeRegistration createSeRegistration(final String typeName) throws IOException { return issue(new Commands.CreateSeRegistrationCommand(typeName)); } /** * @see ISession#createSeTable(java.lang.String) */ public SeTable createSeTable(final String qualifiedName) throws IOException { return issue(new Commands.CreateSeTableCommand(qualifiedName)); } /** * @see ISession#createSeInsert() */ public SeInsert createSeInsert() throws IOException { return issue(Commands.CreateSeInsertCommand); } /** * @see ISession#createSeUpdate() */ public SeUpdate createSeUpdate() throws IOException { return issue(Commands.CreateSeUpdateCommand); } /** * @see ISession#createSeDelete() */ public SeDelete createSeDelete() throws IOException { return issue(Commands.CreateSeDeleteCommand); } /** * @see ISession#describe(java.lang.String) */ public SeColumnDefinition[] describe(final String tableName) throws IOException { final SeTable table = getTable(tableName); return describe(table); } /** * @see ISession#describe(com.esri.sde.sdk.client.SeTable) */ public SeColumnDefinition[] describe(final SeTable table) throws IOException { return issue(new Commands.DescribeTableCommand(table)); } /** * @see ISession#fetch(com.esri.sde.sdk.client.SeQuery) */ public SdeRow fetch(final SeQuery query) throws IOException { return fetch(query, new SdeRow((GeometryFactory) null)); } public SdeRow fetch(final SeQuery query, final SdeRow currentRow) throws IOException { return issue(new Command<SdeRow>() { @Override public SdeRow execute(final ISession session, final SeConnection connection) throws SeException, IOException { SeRow row = query.fetch(); if (row == null) { return null; } else { currentRow.setRow(row); } return currentRow; } }); } /** * @see ISession#close(com.esri.sde.sdk.client.SeState) */ public void close(final SeState state) throws IOException { issue(new Commands.CloseStateCommand(state)); } /** * @see ISession#close(com.esri.sde.sdk.client.SeStreamOp) */ public void close(final SeStreamOp stream) throws IOException { issue(new Commands.CloseStreamCommand(stream)); } /** * @see ISession#createState(com.esri.sde.sdk.client.SeObjectId) */ public SeState createState(final SeObjectId stateId) throws IOException { return issue(new Commands.CreateSeStateCommand(stateId)); } /** * @see ISession#createAndExecuteQuery(java.lang.String[], * com.esri.sde.sdk.client.SeSqlConstruct) */ public SeQuery createAndExecuteQuery(final String[] propertyNames, final SeSqlConstruct sql) throws IOException { return issue(new Commands.CreateAndExecuteQueryCommand(propertyNames, sql)); } /** * Creates either a direct child state of parentStateId, or a sibling being an exact copy of * parentStatId if either the state can't be closed because its in use or parentStateId does not * belong to the current user. */ public SeState createChildState(final long parentStateId) throws IOException { return issue(new Commands.CreateVersionStateCommand(parentStateId)); } private static final class CreateSeConnectionCommand extends Command<SeConnection> { private final ArcSDEConnectionConfig config; private final int sessionId; /** * * @param config * @param sessionId * the session id the connection is to be created for. For exception reporting * purposes only */ private CreateSeConnectionCommand(final ArcSDEConnectionConfig config, final int sessionId) { this.config = config; this.sessionId = sessionId; } @Override public SeConnection execute(final ISession session, final SeConnection connection) throws SeException, IOException { final String serverName = config.getServerName(); final int portNumber = config.getPortNumber(); final String databaseName = config.getDatabaseName(); final String userName = config.getUserName(); final String userPassword = config.getPassword(); NegativeArraySizeException cause = null; SeConnection conn = null; try { for (int i = 0; i < 3; i++) { try { if (LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Creating connection for session #" + sessionId + "(try " + (i + 1) + " of 3)"); } conn = new SeConnection(serverName, portNumber, databaseName, userName, userPassword); //conn.setConcurrency(SeConnection.SE_ONE_THREAD_POLICY); // SeStreamSpec streamSpec = new SeStreamSpec(); // streamSpec.setRasterBufSize(2*128*128); // conn.setStreamSpec(streamSpec); break; } catch (NegativeArraySizeException nase) { LOGGER.warning("Strange failed ArcSDE connection error. " + "Trying again (try " + (i + 1) + " of 3). SessionId: " + sessionId); cause = nase; } } } catch (SeException e) { throw new ArcSdeException("Can't create connection to " + serverName + " for Session #" + sessionId, e); } catch (RuntimeException e) { throw (IOException) new IOException("Can't create connection to " + serverName + " for Session #" + sessionId).initCause(e); } if (cause != null) { throw (IOException) new IOException("Couldn't create ArcSDE connection to " + serverName + " for Session #" + sessionId + " because of strange SDE internal exception. " + " Tried 3 times, giving up.").initCause(cause); } return conn; } } }