/*
* 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.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.geotools.arcsde.ArcSdeException;
import org.geotools.arcsde.session.Command;
import org.geotools.arcsde.session.ISession;
import org.geotools.data.DataSourceException;
import org.geotools.feature.AttributeTypeBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.identity.FeatureId;
import org.opengis.filter.identity.Identifier;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.esri.sde.sdk.client.SeColumnDefinition;
import com.esri.sde.sdk.client.SeConnection;
import com.esri.sde.sdk.client.SeCoordinateReference;
import com.esri.sde.sdk.client.SeDefs;
import com.esri.sde.sdk.client.SeException;
import com.esri.sde.sdk.client.SeExtent;
import com.esri.sde.sdk.client.SeLayer;
import com.esri.sde.sdk.client.SeQuery;
import com.esri.sde.sdk.client.SeQueryInfo;
import com.esri.sde.sdk.client.SeRegistration;
import com.esri.sde.sdk.client.SeRow;
import com.esri.sde.sdk.client.SeShape;
import com.esri.sde.sdk.client.SeTable;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Utility class to deal with SDE specifics such as creating SeQuery objects from geotool's Query's,
* mapping SDE types to Java ones and JTS Geometries, etc.
*
* @author Gabriel Roldan
*
*
* @source $URL$
* http://svn.geotools.org/geotools/trunk/gt/modules/unsupported/arcsde/datastore/src/main
* /java/org/geotools/arcsde/data/ArcSDEAdapter.java $
* @version $Id$
*/
@SuppressWarnings("deprecation")
public class ArcSDEAdapter {
/** Logger for ths class' package */
private static final Logger LOGGER = Logging.getLogger(ArcSDEAdapter.class.getName());
/** mappings of SDE attribute's types to Java ones */
private static final Map<Integer, Class<?>> sde2JavaTypes = new HashMap<Integer, Class<?>>();
/** inverse of sdeTypes, maps Java types to SDE ones */
private static final Map<Class<?>, SdeTypeDef> java2SDETypes = new HashMap<Class<?>, SdeTypeDef>();
static {
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_NSTRING), String.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_STRING), String.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_INT16), Short.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_INT32), Integer.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_INT64), Long.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_FLOAT32), Float.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_FLOAT64), Double.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_DATE), Date.class);
// @TODO: not at all, only for capable open table with GeoServer
// sde2JavaTypes.put(new Integer(SeColumnDefinition.TYPE_BLOB),
// byte[].class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_CLOB), String.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_NCLOB), String.class);
// @TODO sde2JavaTypes.put(new Integer(SeColumnDefinition.TYPE_CLOB),
// String.class);
// @Tested for view
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_UUID), String.class);
// @TODO sde2JavaTypes.put(new Integer(SeColumnDefinition.TYPE_XML),
// org.w3c.dom.Document.class);
// deprecated codes as for ArcSDE 9.0+. Adding them to maintain < 9.0
// compatibility
// though the assigned int codes matched their new counterparts, I let
// them here as a reminder
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_SMALLINT), Short.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_INTEGER), Integer.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_FLOAT), Float.class);
sde2JavaTypes.put(Integer.valueOf(SeColumnDefinition.TYPE_DOUBLE), Double.class);
/**
* By now keep using the deprecated constants (TYPE_INTEGER, etc.), switching directly to
* the new ones gives problems with ArcSDE instances prior to version 9.0.
*/
// SeColumnDefinition.TYPE_RASTER is not supported...
java2SDETypes.put(String.class, new SdeTypeDef(SeColumnDefinition.TYPE_STRING, 255, 0));
java2SDETypes.put(Short.class, new SdeTypeDef(SeColumnDefinition.TYPE_SMALLINT, 4, 0));
java2SDETypes.put(Integer.class, new SdeTypeDef(SeColumnDefinition.TYPE_INTEGER, 10, 0));
java2SDETypes.put(Float.class, new SdeTypeDef(SeColumnDefinition.TYPE_FLOAT, 5, 2));
java2SDETypes.put(Double.class, new SdeTypeDef(SeColumnDefinition.TYPE_DOUBLE, 25, 4));
java2SDETypes.put(Date.class, new SdeTypeDef(SeColumnDefinition.TYPE_DATE, 1, 0));
java2SDETypes.put(Long.class, new SdeTypeDef(SeColumnDefinition.TYPE_INTEGER, 10, 0));
// java2SDETypes.put(byte[].class, new
// SdeTypeDef(SeColumnDefinition.TYPE_BLOB, 1, 0));
java2SDETypes.put(Number.class, new SdeTypeDef(SeColumnDefinition.TYPE_DOUBLE, 25, 4));
}
public static int guessShapeTypes(GeometryDescriptor attribute) {
if (attribute == null) {
throw new NullPointerException("a GeometryAttributeType must be provided, got null");
}
Class<?> geometryClass = attribute.getType().getBinding();
int shapeTypes = 0;
if (attribute.isNillable()) {
shapeTypes |= SeLayer.SE_NIL_TYPE_MASK;
}
if (GeometryCollection.class.isAssignableFrom(geometryClass)) {
shapeTypes |= SeLayer.SE_MULTIPART_TYPE_MASK;
if (geometryClass == MultiPoint.class) {
shapeTypes |= SeLayer.SE_POINT_TYPE_MASK;
} else if (geometryClass == MultiLineString.class) {
shapeTypes |= SeLayer.SE_LINE_TYPE_MASK;
} else if (geometryClass == MultiPolygon.class) {
shapeTypes |= SeLayer.SE_AREA_TYPE_MASK;
} else {
throw new IllegalArgumentException("no SDE geometry mapping for " + geometryClass);
}
} else {
if (geometryClass == Point.class) {
shapeTypes |= SeLayer.SE_POINT_TYPE_MASK;
} else if (geometryClass == LineString.class) {
shapeTypes |= SeLayer.SE_LINE_TYPE_MASK;
} else if (geometryClass == Polygon.class) {
shapeTypes |= SeLayer.SE_AREA_TYPE_MASK;
} else if (geometryClass == Geometry.class) {
LOGGER.fine("Creating SeShape types for all types of geometries.");
shapeTypes |= (SeLayer.SE_MULTIPART_TYPE_MASK | SeLayer.SE_POINT_TYPE_MASK
| SeLayer.SE_LINE_TYPE_MASK | SeLayer.SE_AREA_TYPE_MASK);
} else {
throw new IllegalArgumentException("no SDE geometry mapping for " + geometryClass);
}
}
return shapeTypes;
}
/**
* Creates the column definition as used by the ArcSDE Java API, for the given AttributeType.
*
* @param type
* the source attribute definition.
* @return an <code>SeColumnDefinition</code> object matching the properties of the source
* AttributeType.
* @throws SeException
* if the SeColumnDefinition constructor throws it due to some invalid parameter
*/
public static SeColumnDefinition createSeColumnDefinition(AttributeDescriptor type)
throws SeException {
SeColumnDefinition colDef = null;
String colName = type.getLocalName();
boolean nillable = type.isNillable();
SdeTypeDef def = getSdeType(type.getType().getBinding());
int sdeColType = def.colDefType;
int fieldLength = def.size;
int fieldScale = def.scale;
colDef = new SeColumnDefinition(colName, sdeColType, fieldLength, fieldScale, nillable);
return colDef;
}
/**
*
* @param attClass
* @return an SdeTypeDef instance with default values for the given class
* @throws IllegalArgumentException
*/
private static SdeTypeDef getSdeType(Class<?> attClass) throws IllegalArgumentException {
SdeTypeDef sdeType = java2SDETypes.get(attClass);
if (sdeType == null) {
throw new IllegalArgumentException("No SDE type mapping for " + attClass.getName());
}
return sdeType;
}
// public static FeatureTypeInfo fetchSchema(final String typeName,
// final String namespace,
// final SessionPool pool) throws IOException {
// return pool.issueReadOnly(new Command<FeatureTypeInfo>() {
// @Override
// public FeatureTypeInfo execute(Session session, SeConnection connection)
// throws SeException, IOException {
// return fetchSchema(typeName, namespace, session);
// }
// });
// }
/**
* Fetches the schema of a given ArcSDE featureclass and creates its corresponding Geotools
* FeatureType
*
* @return the feature type info representing the ArcSDE feature class given by the layer and
* table.
* @throws IOException
* if an exception is caught accessing the sde feature class metadata.
*/
public static FeatureTypeInfo fetchSchema(final String typeName, final String namespace,
final ISession session) throws IOException {
final SeTable table = session.getTable(typeName);
SeLayer layer = null;
final SeColumnDefinition[] seColumns = session.describe(typeName);
for (SeColumnDefinition col : seColumns) {
if (col.getType() == SeColumnDefinition.TYPE_SHAPE) {
layer = session.getLayer(typeName);
break;
}
}
final List<AttributeDescriptor> properties = createAttributeDescriptors(layer, namespace,
seColumns);
final SimpleFeatureType featureType = createSchema(typeName, namespace, properties);
SeRegistration registration = session.createSeRegistration(typeName);
final boolean isMultiVersioned = registration.isMultiVersion();
final boolean isView = registration.isView();
final FIDReader fidStrategy;
fidStrategy = FIDReader.getFidReader(session, table, layer, registration);
final boolean canDoTransactions;
{
final Integer permMask = session.issue(new Command<Integer>() {
@Override
public Integer execute(ISession session, SeConnection connection)
throws SeException, IOException {
return new Integer(table.getPermissions());
}
});
final boolean hasWritePermissions = userHasWritePermissions(permMask.intValue());
canDoTransactions = hasWritePermissions
&& (fidStrategy instanceof FIDReader.SdeManagedFidReader || fidStrategy instanceof FIDReader.UserManagedFidReader)
&& !hasReadOnlyColumn(seColumns);
if (hasWritePermissions && !canDoTransactions) {
LOGGER.fine(typeName + " is writable bu has no primary key, thus we're using it "
+ "read-only as can't get a propper feature id out of it");
}
}
FeatureTypeInfo typeInfo = new FeatureTypeInfo(featureType, fidStrategy, canDoTransactions,
isMultiVersioned, isView);
return typeInfo;
}
/**
* Check if any of the column types are read-only (such as CLOB).
* <p>
* This check should be temporary; currently writing CLOB types is producing a segmentation
* fault (gasp!) in ArcSDE 9.3. We imagine the java encoding is not quite what ArcSDE expected.
*
* @param seColumns
* @return true if any of the columns are read-only
*/
private static boolean hasReadOnlyColumn(SeColumnDefinition[] seColumns) {
for (SeColumnDefinition col : seColumns) {
if (col.getType() == SeColumnDefinition.TYPE_CLOB
|| col.getType() == SeColumnDefinition.TYPE_NCLOB) {
return true;
}
}
return false;
}
/**
* Checks wether the user can write to the given {@code table}.
* <p>
* Depends on the proviledges of the user the connection the table was created with.
* </p>
*
* @param permissions
* the sde table permissions mask (as per SeTable.getPermissions())to check for write
* permissions
* @return {@code true} if the table's connection user has both insert, update and delete
* priviledges.
* @throws ArcSdeException
* if an SeException is thrown asking the table for the permissions
*/
private static boolean userHasWritePermissions(final int permissions) throws ArcSdeException {
final int insertMask = SeDefs.SE_INSERT_PRIVILEGE;
final int updateMask = SeDefs.SE_UPDATE_PRIVILEGE;
final int deleteMask = SeDefs.SE_DELETE_PRIVILEGE;
boolean canWrite = false;
if (((insertMask & permissions) == insertMask)
&& ((updateMask & permissions) == updateMask)
&& ((deleteMask & permissions) == deleteMask)) {
canWrite = true;
}
return canWrite;
}
/**
* Creates a schema for the "SQL SELECT" like view definition
*/
public static FeatureTypeInfo createInprocessViewSchema(final ISession session,
final String typeName, final String namespace, final PlainSelect qualifiedSelect,
final SeQueryInfo queryInfo) throws IOException {
List<AttributeDescriptor> attributeDescriptors;
// is the first table is a layer, we'll get it to obtain CRS info
// from
String mainTable;
try {
mainTable = queryInfo.getConstruct().getTables()[0];
} catch (SeException e) {
throw new ArcSdeException(e);
}
SeLayer layer = null;
try {
layer = session.getLayer(mainTable);
} catch (NoSuchElementException e) {
LOGGER.info(mainTable + " is not an SeLayer, so no CRS info will be parsed");
}
LOGGER.fine("testing query");
final Command<SeColumnDefinition[]> testQueryCmd = new Command<SeColumnDefinition[]>() {
@Override
public SeColumnDefinition[] execute(ISession session, SeConnection connection)
throws SeException, IOException {
final SeQuery testQuery = new SeQuery(connection);
try {
testQuery.prepareQueryInfo(queryInfo);
testQuery.execute();
LOGGER.fine("definition query executed successfully");
LOGGER.fine("fetching row to obtain view's types");
SeRow testRow = testQuery.fetch();
SeColumnDefinition[] colDefs = testRow.getColumns();
return colDefs;
} finally {
try {
testQuery.close();
} catch (SeException e) {
throw new ArcSdeException(e);
}
}
}
};
final SeColumnDefinition[] colDefs = session.issue(testQueryCmd);
attributeDescriptors = createAttributeDescriptors(layer, namespace, colDefs);
final SimpleFeatureType type = createSchema(typeName, namespace, attributeDescriptors);
final FIDReader fidStrategy = FIDReader.NULL_READER;
FeatureTypeInfo typeInfo;
typeInfo = new FeatureTypeInfo(type, fidStrategy, qualifiedSelect, queryInfo);
return typeInfo;
}
private static List<AttributeDescriptor> createAttributeDescriptors(SeLayer sdeLayer,
String namespace, SeColumnDefinition[] seColumns) throws DataSourceException {
String attName;
boolean isNilable;
int fieldLen;
Object defValue;
CoordinateReferenceSystem metadata = null;
final int nCols = seColumns.length;
List<AttributeDescriptor> attDescriptors = new ArrayList<AttributeDescriptor>(nCols);
Class<?> typeClass = null;
for (int i = 0; i < nCols; i++) {
SeColumnDefinition colDef = seColumns[i];
// didn't found in the ArcSDE Java API the way of knowing
// if an SeColumnDefinition is nillable
attName = colDef.getName();
isNilable = colDef.allowsNulls();
defValue = null;
fieldLen = colDef.getSize();
final Integer sdeType = Integer.valueOf(colDef.getType());
if (sdeType.intValue() == SeColumnDefinition.TYPE_SHAPE) {
CoordinateReferenceSystem crs = null;
crs = parseCRS(sdeLayer);
metadata = crs;
int seShapeType = sdeLayer.getShapeTypes();
typeClass = getGeometryTypeFromLayerMask(seShapeType);
isNilable = (seShapeType & SeLayer.SE_NIL_TYPE_MASK) == SeLayer.SE_NIL_TYPE_MASK;
defValue = isNilable ? null : ArcSDEGeometryBuilder.defaultValueFor(typeClass);
} else {
typeClass = getJavaBinding(sdeType);
if (typeClass == null) {
LOGGER.info("Found an unsupported ArcSDE data type: " + sdeType
+ " for column " + attName + ". Ignoring it.");
continue;
}
// @TODO: add restrictions once the Restrictions utility methods
// are implemented
// Set restrictions = Restrictions.createLength(name, typeClass,
// fieldLen);
}
final int rowIdType = colDef.getRowIdType();
if (rowIdType == SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_SDE) {
continue; // skip over things we cannot edit modify or otherwise treat as
// attributes
}
AttributeTypeBuilder b = new AttributeTypeBuilder();
// call setDefaultValue before setBinding
b.setDefaultValue(defValue);
b.setBinding(typeClass);
b.setName(attName);
b.setNillable(isNilable);
if (fieldLen > 0) {
b.setLength(fieldLen);
}
// only set CRS if its a geometry type, otherwise
// AttributeTypeBuilder
// creates a GeometryAttributeType disregarding the class binding
if (Geometry.class.isAssignableFrom(typeClass)) {
b.setCRS(metadata);
}
AttributeDescriptor buildDescriptor = b.buildDescriptor(attName);
attDescriptors.add(buildDescriptor);
}
return attDescriptors;
}
/**
* Returns the Java class binding for a given SDE column type.
* <p>
* Mappings are:
* <ul>
* <li>{@link SeColumnDefinition#TYPE_BLOB}: byte[].class <b>this one is pending further
* development, not supported currently but just ignored</b>
* <li>{@link SeColumnDefinition#TYPE_CLOB}: {@link String}.class
* <li>{@link SeColumnDefinition#TYPE_DATE}: {@link Date}.class
* <li>{@link SeColumnDefinition#TYPE_FLOAT32}: {@link Float}.class
* <li>{@link SeColumnDefinition#TYPE_FLOAT64}: {@link Double}.class
* <li>{@link SeColumnDefinition#TYPE_INT16}: {@link Short}.class
* <li>{@link SeColumnDefinition#TYPE_INT32}: {@link Integer}.class
* <li>{@link SeColumnDefinition#TYPE_INT64}: {@link Long}.class
* <li>{@link SeColumnDefinition#TYPE_NCLOB}: {@link String}.class
* <li>{@link SeColumnDefinition#TYPE_NSTRING}: {@link String}.class
* <li>{@link SeColumnDefinition#TYPE_UUID}: {@link String}.class
* </ul>
* </p>
* <p>
* Currently <b>there're no</b> bindings defined for:
* <ul>
* <li>{@link SeColumnDefinition#TYPE_XML}
* <li>{@link SeColumnDefinition#TYPE_RASTER}
* </ul>
* </p>
* <p>
* To obtain the JTS Geometry class binding for an sde column of type
* {@link SeColumnDefinition#TYPE_SHAPE} use {@link #getGeometryTypeFromLayerMask(int)}.
* </p>
*
* @param sdeType
* @return the java binding for the given sde data type or {@code null} if its not supported
*/
public static Class<?> getJavaBinding(final Integer sdeType) {
Class<?> javaClass = sde2JavaTypes.get(sdeType);
return javaClass;
}
private static SimpleFeatureType createSchema(final String typeName, final String namespace,
final List<AttributeDescriptor> properties) throws IOException {
// TODO: use factory lookup mechanism once its in place
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.setName(typeName);
builder.setNamespaceURI(namespace);
for (Iterator<AttributeDescriptor> it = properties.iterator(); it.hasNext();) {
AttributeDescriptor attType = it.next();
builder.add(attType);
}
return builder.buildFeatureType();
}
/**
* Obtains the <code>SeCoordinateReference</code> of the given <code>SeLayer</code> and tries to
* create a <code>org.opengis.referencing.crs.CoordinateReferenceSystem</code> from its WKT.
*
* @param sdeLayer
* the SeLayer from which to query the CRS in ArcSDE form.
* @return the actual CRS or null if <code>sdeLayer</code> does not defines its coordinate
* system.
* @throws DataSourceException
* if the WKT can't be parsed to an opengis CRS using the CRSFactory
*/
private static CoordinateReferenceSystem parseCRS(SeLayer sdeLayer) throws DataSourceException {
CoordinateReferenceSystem crs = null;
SeCoordinateReference seCRS = sdeLayer.getCoordRef();
String WKT = seCRS.getProjectionDescription();
LOGGER.finer("About to parse CRS for layer " + sdeLayer.getName() + ": " + WKT);
try {
LOGGER.fine(sdeLayer.getName() + " has CRS envelope: " + seCRS.getXYEnvelope());
} catch (SeException e1) {
// intentionally blank
}
if ("UNKNOWN".equalsIgnoreCase(WKT)) {
LOGGER.fine("ArcSDE layer " + sdeLayer.getName()
+ " does not provides a Coordinate Reference System");
} else {
try {
CRSFactory crsFactory = ReferencingFactoryFinder.getCRSFactory(null);
crs = crsFactory.createFromWKT(WKT);
LOGGER.fine("ArcSDE CRS correctly parsed from layer " + sdeLayer.getName());
} catch (FactoryException e) {
String msg = "CRS factory does not knows how to parse the " + "CRS for layer "
+ sdeLayer.getName() + ": " + WKT;
LOGGER.log(Level.CONFIG, msg, e);
// throw new DataSourceException(msg, e);
}
}
return crs;
}
/**
* Returns the mapping JTS geometry type for the ArcSDE Shape type given by the bitmask
* <code>seShapeType</code>
* <p>
* This bitmask is composed of a combination of the following shape types, as defined in the
* ArcSDE Java API:
*
* <pre>
* SE_NIL_TYPE_MASK = 1;
* SE_POINT_TYPE_MASK = 2;
* SE_LINE_TYPE_MASK = 4;
* SE_AREA_TYPE_MASK = 16;
* SE_MULTIPART_TYPE_MASK = 262144;
* </pre>
*
* (Note that the type SE_SIMPLE_LINE_TYPE_MASK is not used)
* </p>
*
* @param seShapeType
* @return
* @throws IllegalArgumentException
*/
public static Class<? extends Geometry> getGeometryTypeFromLayerMask(int seShapeType) {
Class<? extends Geometry> clazz = com.vividsolutions.jts.geom.Geometry.class;
final int MULTIPART_MASK = SeLayer.SE_MULTIPART_TYPE_MASK;
final int POINT_MASK = SeLayer.SE_POINT_TYPE_MASK;
final int SIMPLE_LINE_MASK = SeLayer.SE_SIMPLE_LINE_TYPE_MASK;
final int LINESTRING_MASK = SeLayer.SE_LINE_TYPE_MASK;
final int AREA_MASK = SeLayer.SE_AREA_TYPE_MASK;
// if (seShapeType == SeLayer.TYPE_NIL) {
// // do nothing
// } else if (seShapeType == SeLayer.TYPE_MULTI_MASK) {
// clazz = GeometryCollection.class;
// } else if (seShapeType == SeLayer.TYPE_LINE || seShapeType ==
// SeLayer.TYPE_SIMPLE_LINE) {
// clazz = LineString.class;
// } else if (seShapeType == SeLayer.TYPE_MULTI_LINE
// || seShapeType == SeLayer.TYPE_MULTI_SIMPLE_LINE) {
// clazz = MultiLineString.class;
// } else if (seShapeType == SeLayer.TYPE_MULTI_POINT) {
// clazz = MultiPoint.class;
// } else if (seShapeType == SeLayer.TYPE_MULTI_POLYGON) {
// clazz = MultiPolygon.class;
// } else if (seShapeType == SeLayer.TYPE_POINT) {
// clazz = Point.class;
// } else if (seShapeType == SeLayer.TYPE_POLYGON) {
// clazz = Polygon.class;
// } else {
// in all this assignments, 1 means true and 0 false
final int isCollection = ((seShapeType & MULTIPART_MASK) == MULTIPART_MASK) ? 1 : 0;
final int isPoint = ((seShapeType & POINT_MASK) == POINT_MASK) ? 1 : 0;
final int isLineString = (((seShapeType & SIMPLE_LINE_MASK) == SIMPLE_LINE_MASK) || ((seShapeType & LINESTRING_MASK) == LINESTRING_MASK)) ? 1
: 0;
final int isPolygon = ((seShapeType & AREA_MASK) == AREA_MASK) ? 1 : 0;
// first check if the shape type supports more than one geometry
// type.
// In that case, it is *highly* recomended that it support all the
// geometry types, so we can safely return Geometry.class. If this
// is not the case and the shape type supports just a few geometry types,
// then we give it a chance and return Geometry.class anyway, but be
// aware that transactions over that layer could fail if a Geometry that
// is not supported is tried for insertion.
if ((isPoint + isLineString + isPolygon) > 1) {
clazz = Geometry.class;
if (4 < (isCollection + isPoint + isLineString + isPolygon)) {
LOGGER.warning("Be careful!! we're mapping an ArcSDE Shape type "
+ "to the generic Geometry class, but the shape type "
+ "does not really allows all geometry types!: " + "isCollection="
+ isCollection + ", isPoint=" + isPoint + ", isLineString=" + isLineString
+ ", isPolygon=" + isPolygon);
} else {
LOGGER.fine("safely mapping SeShapeType to abstract Geometry");
}
} else if (isCollection == 1) {
if (isPoint == 1) {
clazz = MultiPoint.class;
} else if (isLineString == 1) {
clazz = MultiLineString.class;
} else if (isPolygon == 1) {
clazz = MultiPolygon.class;
} else {
throw new IllegalStateException("this shouldn't happen!");
}
} else {
if (isPoint == 1) {
clazz = Point.class;
} else if (isLineString == 1) {
clazz = LineString.class;
} else if (isPolygon == 1) {
clazz = Polygon.class;
} else {
throw new IllegalStateException("this shouldn't happen!");
}
}
// }
return clazz;
}
/**
* Returns the most appropriate {@link Geometry} class that matches the shape's type.
*
* @param shape
* SeShape instance for which to infer the matching geometry class, can't be null
* @return the Geometry subclass corresponding to the shape type
* @throws SeException
* propagated if thrown by {@link SeShape#getType()}
* @throws IllegalArgumentException
* if none of the JTS geometry classes can be matched to the shape type (shouldnt
* happen as for the {@link SeShape#getType() types} defined in the esri arcsde java
* api 9.0)
*/
public static Class<? extends Geometry> getGeometryTypeFromSeShape(SeShape shape)
throws SeException {
final Class<? extends Geometry> clazz;
final int seShapeType = shape.getType();
if (seShapeType == SeShape.TYPE_LINE || seShapeType == SeShape.TYPE_SIMPLE_LINE) {
clazz = LineString.class;
} else if (seShapeType == SeShape.TYPE_MULTI_LINE
|| seShapeType == SeShape.TYPE_MULTI_SIMPLE_LINE) {
clazz = MultiLineString.class;
} else if (seShapeType == SeShape.TYPE_MULTI_POINT) {
clazz = MultiPoint.class;
} else if (seShapeType == SeShape.TYPE_MULTI_POLYGON) {
clazz = MultiPolygon.class;
} else if (seShapeType == SeShape.TYPE_POINT) {
clazz = Point.class;
} else if (seShapeType == SeShape.TYPE_POLYGON) {
clazz = Polygon.class;
} else {
throw new IllegalArgumentException("Cannot map the shape type '" + seShapeType
+ "' to any known SeShape.TYPE_*");
}
return clazz;
}
/**
* Returns the numeric identifier of a FeatureId, given by the full qualified name of the
* featureclass prepended to the ArcSDE feature id. ej: SDE.SDE.SOME_LAYER.1
*
* @param id
* a geotools FeatureID
* @return an ArcSDE feature ID
* @throws IllegalArgumentException
* If the given string is not properly formatted [anystring].[long value]
*/
public static long getNumericFid(Identifier id) throws IllegalArgumentException {
if (!(id instanceof FeatureId))
throw new IllegalArgumentException(
"Only FeatureIds are supported when encoding id filters to SDE. Not "
+ id.getClass());
final String fid = ((FeatureId) id).getID();
return getNumericFid(fid);
}
/**
* Returns the numeric identifier of a FeatureId, given by the full qualified name of the
* featureclass prepended to the ArcSDE feature id. ej: SDE.SDE.SOME_LAYER.1
*
* @param id
* a geotools FeatureID
* @return an ArcSDE feature ID
* @throws IllegalArgumentException
* If the given string is not properly formatted [anystring].[long value]
*/
public static long getNumericFid(String fid) throws IllegalArgumentException {
int dotIndex = fid.lastIndexOf('.');
try {
return Long.decode(fid.substring(++dotIndex)).longValue();
} catch (Exception ex) {
throw new IllegalArgumentException("FeatureID " + fid
+ " does not seems as a valid ArcSDE FID");
}
}
public static long[] getNumericFids(Set<Identifier> identifiers)
throws IllegalArgumentException {
int nfids = identifiers.size();
long[] fids = new long[nfids];
Iterator<Identifier> ids = identifiers.iterator();
for (int i = 0; i < nfids; i++) {
fids[i] = ArcSDEAdapter.getNumericFid(ids.next());
}
return fids;
}
/**
* Holds default values for the properties (size and scale) of a SeColumnDefinition, given by
* its column type (SeColumnDefinition.SE_STRING, etc).
*
* @author Gabriel Roldan, Axios Engineering
* @version $Revision: 1.4 $
*/
private static class SdeTypeDef {
/**
* A magic number provided by SeColumnDefinition (example SeColumnDefinition.TYPE_STRING)
*/
final int colDefType;
/** size (probably field size?) */
final int size;
/** scale */
final int scale;
/**
* Creates a new SdeTypeDef object.
*
* @param colDefType
* Constant provided by SeColumnDefinition
* @param size
* Field size
* @param scale
* Field scale
*/
public SdeTypeDef(int colDefType, int size, int scale) {
this.colDefType = colDefType;
this.size = size;
this.scale = scale;
}
/**
* Text representation
*
* @return Text represenation for debugging
*/
@Override
public String toString() {
return "SdeTypeDef[colDefType=" + this.colDefType + ", size=" + this.size + ", scale="
+ this.scale + "]";
}
}
/**
* Creates the given featuretype in the underlying ArcSDE database.
* <p>
* The common use case to create an ArcSDE layer is to setup the SeTable object with all the
* non-geometry attributes first, then create the SeLayer and set the geometry column name and
* its properties. This approach brings a nice problem, since we need to create the attributes
* in exactly the same order as specified in the passed FeatureType, which means that the
* geometry attribute needs not to be the last one.
* </p>
* <p>
* To avoid this, the following workaround is performed: instead of creating the schema as
* described above, we will first create the SeTable with a single, temporary column, since it
* is not possible to create a table without columns. The, we will iterate over the
* AttributeTypes and add them as they appear using
* <code>SeTable.addColumn(SeColumnDefinition)</code>. But if we found that the current
* AttributeType is geometric, instead of adding the column we just create the SeLayer object.
* This way, the geometric attribute is inserted at the end, and then we keep iterating and
* adding the rest of the columns. Finally, the first column is removed, since it was temporary
* (note that I advertise it, it is a _workaround_).
* </p>
* <p>
* Sometimes some 'extra' information is required to correctly create the underlying ArcSDE
* SeLayer. For instance, a specific configuration keyword might be required to be used (instead
* of "DEFAULTS"), or a particular column might need to be marked as the rowid column for the
* featuretype. A non-null <code>hints</code> parameter contains a mapping from a list of
* well-known {@link java.lang.String} keys to values. The possible keys are listed in the table
* below. keys with any other values are ignored.
* <table>
* <tr>
* <td>key name</td>
* <td>key value type</td>
* <td>default value (if applicable)</td>
* </tr>
* <tr>
* <td>configuration.keyword</td>
* <td>{@link java.lang.String}</td>
* <td>"DEFAULTS"</td>
* </tr>
* <tr>
* <td>rowid.column.type</td>
* <td>{@link java.lang.String} - "NONE", "USER" and "SDE" are the only valid values</td>
* <td>"NONE"</td>
* </tr>
* <tr>
* <td>rowid.column.name</td>
* <td>{@link java.lang.String}</td>
* <td>null</td>
* </tr>
* </p>
*
* @param featureType
* the feature type containing the name, attributes and coordinate reference system
* of the new ArcSDE layer.
* @param hints
* A map containing extra ArcSDE-specific hints about how to create the underlying
* ArcS DE SeLayer and SeTable objects from this FeatureType.
* @param session
* connection to use in order to create the layer and table on the server. The
* connection shall be managed by this method caller.
* @throws IOException
* see <code>throws DataSourceException</code> bellow
* @throws IllegalArgumentException
* if the passed feature type does not contains at least one geometric attribute, or
* if the type name contains '.' (dots).
* @throws NullPointerException
* if <code>featureType</code> is <code>null</code>
* @throws DataSourceException
* if there is <b>not an available (free) connection </b> to the ArcSDE instance(in
* that case maybe you need to increase the maximun number of connections for the
* connection pool), or an SeException exception is catched while creating the
* feature type at the ArcSDE instance (e.g. a table with that name already exists).
*/
public static void createSchema(final SimpleFeatureType featureType,
final Map<String, String> hints, final ISession session) throws IOException,
IllegalArgumentException {
if (featureType == null) {
throw new NullPointerException("You have to provide a FeatureType instance");
}
final Command<Void> createSchemaCmd = new Command<Void>() {
@Override
public Void execute(ISession session, SeConnection connection) throws SeException,
IOException {
final String[] typeNameParts = featureType.getTypeName().split("\\.");
final String unqualifiedTypeName = typeNameParts[typeNameParts.length - 1];
// Create a new SeTable/SeLayer with the specified attributes....
SeTable table = null;
SeLayer layer = null;
// flag to know if the table was created by us when catching an
// exception.
boolean tableCreated = false;
// table/layer creation hints information
int rowIdType = SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_NONE;
String rowIdColumn = null;
String configKeyword = "DEFAULTS";
if (hints.containsKey("configuration.keyword")) {
configKeyword = String.valueOf(hints.get("configuration.keyword"));
}
if (hints.get("rowid.column.type") instanceof String) {
String rowIdStr = (String) hints.get("rowid.column.type");
if (rowIdStr.equalsIgnoreCase("NONE")) {
rowIdType = SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_NONE;
} else if (rowIdStr.equalsIgnoreCase("USER")) {
rowIdType = SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_USER;
} else if (rowIdStr.equalsIgnoreCase("SDE")) {
rowIdType = SeRegistration.SE_REGISTRATION_ROW_ID_COLUMN_TYPE_SDE;
} else {
throw new DataSourceException(
"createSchema hint 'rowid.column.type' must be one of 'NONE', 'USER' or 'SDE'");
}
}
if (hints.get("rowid.column.name") instanceof String) {
rowIdColumn = (String) hints.get("rowid.column.name");
}
// placeholder to a catched exception to know in the finally block
// if we should cleanup the crap we left in the database
Exception error = null;
try {
// create a table with provided username
String qualifiedName = null;
if (unqualifiedTypeName.indexOf('.') == -1) {
// Use the already parsed name (unqualifiedTypeName)
qualifiedName = connection.getUser() + "." + unqualifiedTypeName; // featureType.getTypeName();
LOGGER.finer("new full qualified type name: " + qualifiedName);
} else {
qualifiedName = unqualifiedTypeName;
LOGGER.finer("full qualified type name provided by user: " + qualifiedName);
}
layer = new SeLayer(connection);
layer.setTableName(qualifiedName);
layer.setCreationKeyword(configKeyword);
final String HACK_COL_NAME = "gt_workaround_col_";
table = createSeTable(connection, qualifiedName, HACK_COL_NAME, configKeyword);
tableCreated = true;
final List<AttributeDescriptor> atts = featureType.getAttributeDescriptors();
AttributeDescriptor currAtt;
for (Iterator<AttributeDescriptor> it = atts.iterator(); it.hasNext();) {
currAtt = it.next();
if (currAtt instanceof GeometryDescriptor) {
GeometryDescriptor geometryAtt = (GeometryDescriptor) currAtt;
createSeLayer(layer, qualifiedName, geometryAtt);
} else {
LOGGER.fine("Creating column definition for " + currAtt);
SeColumnDefinition newCol = ArcSDEAdapter
.createSeColumnDefinition(currAtt);
// /////////////////////////////////////////////////////////////
// HACK!!!!: this hack is just to avoid the error that
// occurs //
// when adding a column wich is not nillable. Need to fix
// this//
// but by now it conflicts with the requirement of creating
// //
// the schema with the correct attribute order. //
// /////////////////////////////////////////////////////////////
newCol = new SeColumnDefinition(newCol.getName(), newCol.getType(),
newCol.getSize(), newCol.getScale(), true);
// /////////////////////////////////////////////////////////////
// END of horrible HACK //
// /////////////////////////////////////////////////////////////
LOGGER.fine("Adding column " + newCol.getName()
+ " to the actual table.");
table.addColumn(newCol);
}
}
LOGGER.fine("deleting the 'workaround' column...");
table.dropColumn(HACK_COL_NAME);
LOGGER.fine("setting up table registration with ArcSDE...");
SeRegistration reg = new SeRegistration(connection, table.getName());
if (rowIdColumn != null) {
LOGGER.fine("setting rowIdColumnName to " + rowIdColumn + " in table "
+ reg.getTableName());
reg.setRowIdColumnName(rowIdColumn);
reg.setRowIdColumnType(rowIdType);
reg.alter();
reg = null;
}
LOGGER.fine("Schema correctly created: " + featureType);
} catch (SeException e) {
LOGGER.log(Level.WARNING, e.getSeError().getErrDesc(), e);
throw e;
} finally {
if ((error != null) && tableCreated) {
// TODO: remove table if created and then failed
}
}
return null;
}
};
session.issue(createSchemaCmd);
}
/**
* Creates a new table in the server. Warning warning, this method shall only be called from
* inside a {@link Command}
*
* @param connection
* @param qualifiedName
* @param hackColName
* @param configKeyword
* @return
* @throws IOException
* @throws SeException
*/
private static SeTable createSeTable(final SeConnection connection, final String qualifiedName,
final String hackColName, final String configKeyword) throws SeException {
final SeTable table;
final SeColumnDefinition[] tmpCol = new SeColumnDefinition[1];
tmpCol[0] = new SeColumnDefinition(hackColName, SeColumnDefinition.TYPE_STRING, 4, 0, true);
table = new SeTable(connection, qualifiedName);
LOGGER.info("creating table " + qualifiedName);
// create the table using DBMS default configuration keyword.
// valid keywords are defined in the dbtune table.
table.create(tmpCol, configKeyword);
LOGGER.info("table " + qualifiedName + " created...");
return table;
}
private static void createSeLayer(SeLayer layer, String qualifiedName,
GeometryDescriptor geometryAtt) throws SeException {
String spatialColName = geometryAtt.getLocalName();
LOGGER.info("setting spatial column name: " + spatialColName);
layer.setSpatialColumnName(spatialColName);
// Set the shape types that can be inserted into this layer
int seShapeTypes = ArcSDEAdapter.guessShapeTypes(geometryAtt);
layer.setShapeTypes(seShapeTypes);
layer.setGridSizes(1100.0, 0.0, 0.0);
layer.setDescription("Created with GeoTools");
// Define the layer's Coordinate Reference
CoordinateReferenceSystem crs = geometryAtt.getCoordinateReferenceSystem();
SeCoordinateReference coordref = getGenericCoordRef();
String WKT = null;
if (crs == null) {
LOGGER.warning("Creating feature type " + qualifiedName
+ ": the geometry attribute does not supply a coordinate reference system");
} else {
LOGGER.info("Creating the SeCoordRef object for CRS " + crs);
WKT = crs.toWKT();
coordref.setCoordSysByDescription(WKT);
}
SeExtent validCoordRange = null;
if ((WKT != null) && (WKT.indexOf("GEOGCS") != -1)) {
validCoordRange = new SeExtent(-180, -90, 180, 90);
} else {
validCoordRange = coordref.getXYEnvelope();
}
layer.setExtent(validCoordRange);
LOGGER.info("Applying CRS " + coordref.getCoordSysDescription());
layer.setCoordRef(coordref);
LOGGER.info("CRS applyed to the new layer.");
// /////////////////////////
// this param is used by ArcSDE for database initialization purposes
int estInitFeatCount = 4;
// this param is used by ArcSDE as an estimation of the average number
// of points the layer's geometries will have, one never will know what
// for
int estAvgPointsPerFeature = 4;
LOGGER.info("Creating the layer...");
layer.create(estInitFeatCount, estAvgPointsPerFeature);
LOGGER.info("ArcSDE layer created.");
}
/**
* Creates and returns a <code>SeCoordinateReference</code> CRS, though based on an UNKNOWN CRS,
* is inclusive enough (in terms of valid coordinate range and presicion) to deal with most
* coordintates.
* <p>
* Actually tested to deal with coordinates with 0.0002 units of separation as well as with
* large coordinates such as UTM (values greater than 500,000.00)
* </p>
* <p>
* This method is driven by the equally named method in TestData.java
* </p>
*
* @return
* @throws SeException
*/
private static SeCoordinateReference getGenericCoordRef() throws SeException {
// create a sde CRS with a huge value range and 5 digits of presission
SeCoordinateReference seCRS = new SeCoordinateReference();
int shift = 600000;
SeExtent validRange = new SeExtent(-shift, -shift, shift, shift);
seCRS.setXYByEnvelope(validRange);
LOGGER.info("CRS: " + seCRS.getXYEnvelope());
return seCRS;
}
}