Package org.geotools.arcsde.data

Source Code of org.geotools.arcsde.data.ArcSDEAdapter$SdeTypeDef

/*
*    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;
    }
}
TOP

Related Classes of org.geotools.arcsde.data.ArcSDEAdapter$SdeTypeDef

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.