/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2009, 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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.geotools.arcsde.session.Command;
import org.geotools.arcsde.session.ISession;
import org.geotools.arcsde.session.ISessionPool;
import org.geotools.arcsde.session.UnavailableConnectionException;
import org.geotools.data.DataSourceException;
import org.geotools.feature.NameImpl;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.Name;
import com.esri.sde.sdk.client.SeConnection;
import com.esri.sde.sdk.client.SeDefs;
import com.esri.sde.sdk.client.SeError;
import com.esri.sde.sdk.client.SeException;
import com.esri.sde.sdk.client.SeLayer;
import com.esri.sde.sdk.client.SeRegistration;
import com.esri.sde.sdk.client.SeTable;
/**
* Maintains a cache of {@link FeatureTypeInfo} objects for fast retrieval of ArcSDE vector layer
* information and its corresponding geotools {@link FeatureType}.
* <p>
* {@link SeLayer} objects are not cached, they hold a reference to its connection and hence may
* only be used inside the connection's context. Instead, a set of layer names is kept and the set
* of actual {@link FeatureTypeInfo}s is lazily loaded on demand.
* </p>
* <p>
* This class may set up a background process to periodically update the list of available layer
* names in the server and clear the feature type cache. See the constructor's javadoc for more
* info.
* </p>
*
* @author Gabriel Roldan
* @version $Id$
* @since 2.5.6
* @source $URL:
* http://svn.osgeo.org/geotools/trunk/modules/plugin/arcsde/datastore/src/main/java/org
* /geotools/arcsde/data/FeatureTypeInfoCache.java $
*/
final class FeatureTypeInfoCache {
private static final Logger LOGGER = Logging.getLogger("org.geotools.arcsde.data");
/**
* ArcSDE registered layers definitions
*/
private final Map<String, FeatureTypeInfo> featureTypeInfos;
/**
* In process view definitions. This map is populated through
* {@link #registerView(String, PlainSelect)}
*/
private final Map<String, FeatureTypeInfo> inProcessFeatureTypeInfos;
private final ISessionPool sessionPool;
/**
* list of available featureclasses in the database. Does not contain in-process view type
* names. SeLayer objects are not cached because they hold a reference to its SeConnection and
* hence need to be used only inside its connection context.
*/
private final Set<String> availableLayerNames;
/**
* Namespace URI to construct FeatureTypes and AttributeTypes with
*/
private final String namespace;
/**
* Scheduler for cache updating.
*/
private ScheduledExecutorService cacheUpdateScheduler;
/**
* Lock for protecting featureTypeInfos cache.
*/
private final ReentrantReadWriteLock cacheLock;
private final boolean allowNonSpatialTables;
private final long cacheUpdateFreqSecs;
/**
* Creates a FeatureTypeInfoCache
* <p>
* The provided {@link ISessionPool} is used to grab an {@link ISession} when the list of
* available layers needs to be updated. This update happens at this class' construction time
* and, optionally, every {@code cacheUpdateFreqSecs} seconds.
* </p>
*
* @param sessionPool
* @param namespace
* the namespace {@link FeatureType}s are created with, may be {@code null}
* @param cacheUpdateFreqSecs
* layer name cache update frequency, in seconds. {@code <= 0} means do never update.
* @param allowNonSpatialTables
* whether non spatial table names are requested
* @throws IOException
*/
public FeatureTypeInfoCache(final ISessionPool sessionPool, final String namespace,
final int cacheUpdateFreqSecs, boolean allowNonSpatialTables) throws IOException {
availableLayerNames = new TreeSet<String>();
featureTypeInfos = new HashMap<String, FeatureTypeInfo>();
inProcessFeatureTypeInfos = new HashMap<String, FeatureTypeInfo>();
this.sessionPool = sessionPool;
this.allowNonSpatialTables = allowNonSpatialTables;
this.namespace = namespace;
this.cacheLock = new ReentrantReadWriteLock();
this.cacheUpdateFreqSecs = cacheUpdateFreqSecs;
reset();
}
public void reset() {
dispose();
CacheUpdater cacheUpdater = new CacheUpdater();
// run now, populate table name cache and then register for periodic running
cacheUpdater.run();
if (cacheUpdateFreqSecs > 0) {
cacheUpdateScheduler = Executors.newScheduledThreadPool(1);
LOGGER.info("Scheduling the layer name cache to be updated every "
+ this.cacheUpdateFreqSecs + " seconds.");
cacheUpdateScheduler.scheduleWithFixedDelay(cacheUpdater, this.cacheUpdateFreqSecs,
this.cacheUpdateFreqSecs, TimeUnit.SECONDS);
} else {
cacheUpdateScheduler = null;
}
}
public boolean isAllowNonSpatialTables() {
return allowNonSpatialTables;
}
public void dispose() {
if (cacheUpdateScheduler != null) {
LOGGER.info("Shutting down cache update scheduler");
cacheUpdateScheduler.shutdownNow();
}
}
public void addInprocessViewInfo(final FeatureTypeInfo typeInfo) {
inProcessFeatureTypeInfos.put(typeInfo.getFeatureTypeName(), typeInfo);
}
public String getNamesapceURI() {
return namespace;
}
public List<String> getTypeNames() {
cacheLock.readLock().lock();
List<String> layerNames;
try {
layerNames = new ArrayList<String>(availableLayerNames);
} finally {
cacheLock.readLock().unlock();
}
layerNames.addAll(this.inProcessFeatureTypeInfos.keySet());
Collections.sort(layerNames);
return layerNames;
}
public List<Name> getNames() {
final List<String> typeNames = getTypeNames();
List<Name> names = new ArrayList<Name>(typeNames.size());
for (String typeName : typeNames) {
NameImpl name = namespace == null ? new NameImpl(typeName) : new NameImpl(namespace,
typeName);
names.add(name);
}
return names;
}
/**
* Check inProcessFeatureTypeInfos and featureTypeInfos for the provided typeName, checking the
* ArcSDE server as a last resort.
*
* @param typeName
* @return
*/
public FeatureTypeInfo getFeatureTypeInfo(final String typeName) throws IOException {
FeatureTypeInfo typeInfo = getCachedTypeInfo(typeName);
if (typeInfo != null) {
return typeInfo;
}
ISession session;
try {
session = sessionPool.getSession(false);
} catch (UnavailableConnectionException e) {
throw new RuntimeException("Can't get type info for " + typeName
+ ". Connection pool exhausted", e);
}
try {
typeInfo = getFeatureTypeInfo(typeName, session);
} finally {
session.dispose();
}
return typeInfo;
}
/**
* Used by feature reader and writer to get the schema information.
* <p>
* They are making use of this function because they already have their own Session to request
* the ftInfo if needed.
* </p>
*
* @param typeName
* @param session
* @return
* @throws IOException
*/
public FeatureTypeInfo getFeatureTypeInfo(final String typeName, final ISession session)
throws IOException {
FeatureTypeInfo typeInfo = getCachedTypeInfo(typeName);
if (typeInfo != null) {
return typeInfo;
}
cacheLock.writeLock().lock();
// Recheck so it hasn't been done already.
try {
typeInfo = featureTypeInfos.get(typeName);
if (typeInfo == null) {
typeInfo = ArcSDEAdapter.fetchSchema(typeName, this.namespace, session);
featureTypeInfos.put(typeName, typeInfo);
}
} finally {
cacheLock.writeLock().unlock();
}
return typeInfo;
}
/**
* @param typeName
* @return the cached type info if there's one for typeName, {@code null} otherwise
* @throws DataSourceException
*/
private FeatureTypeInfo getCachedTypeInfo(final String typeName) throws DataSourceException {
FeatureTypeInfo typeInfo = inProcessFeatureTypeInfos.get(typeName);
if (typeInfo != null) {
return typeInfo;
}
// Check if this is a known featureType
cacheLock.readLock().lock();
try {
if (!availableLayerNames.contains(typeName)) {
throw new DataSourceException(typeName + " does not exist");
}
typeInfo = featureTypeInfos.get(typeName);
} finally {
cacheLock.readLock().unlock();
}
return typeInfo;
}
private final class CacheUpdater implements Runnable {
public void run() {
LOGGER.finer("FeatureTypeCache background process running...");
List<String> typeNames;
try {
typeNames = fetchRegistrations();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Updating TypeNameCache failed.", e);
return;
}
final Set<String> removed;
{// just some logging..
cacheLock.readLock().lock();
Set<String> added = new TreeSet<String>(typeNames);
added.removeAll(availableLayerNames);
if (added.size() > 0) {
LOGGER.finest("FeatureTypeCache: added the following layers: " + added);
}
removed = new TreeSet<String>(availableLayerNames);
removed.removeAll(typeNames);
if (removed.size() > 0) {
LOGGER.finest("FeatureTypeCache: the following layers are no "
+ "longer available: " + removed);
}
cacheLock.readLock().unlock();
}
cacheLock.writeLock().lock();
availableLayerNames.clear();
availableLayerNames.addAll(typeNames);
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("FeatureTypeCache: updated server layer list: " + typeNames);
}
// discard any removed feature type
for (String typeName : removed) {
LOGGER.fine("Removing FeatureTypeInfo for layer " + typeName
+ " since it does no longer exist on the database");
featureTypeInfos.remove(typeName);
}
LOGGER.finer("Finished updated type name cache");
cacheLock.writeLock().unlock();
}
private List<String> fetchRegistrations() throws Exception {
final List<String> typeNames;
final ISession session = sessionPool.getSession(false);
try {
typeNames = session.issue(new FetchRegistrationsCommand(allowNonSpatialTables));
} finally {
session.dispose();
}
return typeNames;
}
}
private static final class FetchRegistrationsCommand extends Command<List<String>> {
private final boolean allowNonSpatialTables;
public FetchRegistrationsCommand(boolean allowNonSpatialTables) {
this.allowNonSpatialTables = allowNonSpatialTables;
}
@SuppressWarnings("unchecked")
@Override
public List<String> execute(ISession session, SeConnection connection) throws SeException,
IOException {
/*
* Note we could do almost the same by calling
* connection.getRegisteredTables():Vector<SeRegistration> but SeRegistration does not
* have a getQualifiedName() method so I fear we can loose ability to serve feature
* types from different users. So first getting the list of all the tables with select
* privilege and then checking if it's registered...
*/
final List<SeTable> registeredTables = connection.getTables(SeDefs.SE_SELECT_PRIVILEGE);
/*
* Get the list of raster tables so they're ignored as feature types
*/
final List<String> rasterColumns = session.getRasterColumns();
final List<String> typeNames = new ArrayList<String>(registeredTables.size());
for (SeTable table : registeredTables) {
String tableName = table.getQualifiedName().toUpperCase();
SeRegistration reg;
try {
reg = new SeRegistration(connection, tableName);
// do not call getInfo or it failst with tables owned by other user than the
// connection one
// reg.getInfo();
} catch (SeException e) {
if (e.getSeError().getSdeError() == SeError.SE_TABLE_NOREGISTERED) {
LOGGER.finest("Ignoring non registered table " + tableName);
continue;
}
throw e;
}
boolean isSystemTable = reg.getRowIdAllocation() == SeRegistration.SE_REGISTRATION_ROW_ID_ALLOCATION_SINGLE;
if (isSystemTable) {
LOGGER.finer("Ignoring ArcSDE registered table " + tableName
+ " as it is a system table");
continue;
}
if (reg.isHidden()) {
LOGGER.finer("Ignoring ArcSDE registered table " + tableName
+ " as it is hidden");
continue;
}
boolean hasLayer = reg.hasLayer();
if (!hasLayer) {
if (!allowNonSpatialTables) {
LOGGER.finer("Ignoring ArcSDE registered table " + tableName
+ " as it is non spatial");
continue;
}
if (reg.getRowIdColumnType() == SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_NONE) {
LOGGER.finer("Ignoring ArcSDE registered table " + tableName
+ " as it has no row id column");
continue;
}
}
if (!rasterColumns.contains(tableName)) {
typeNames.add(tableName);
}
}
return typeNames;
}
}
}