    001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.4_testing/src/org/deegree/model/feature/Validator.java $
    002    /*----------------------------------------------------------------------------
    003     This file is part of deegree, http://deegree.org/
    004     Copyright (C) 2001-2009 by:
    005       Department of Geography, University of Bonn
    006     and
    007       lat/lon GmbH
    008
    009     This library is free software; you can redistribute it and/or modify it under
    010     the terms of the GNU Lesser General Public License as published by the Free
    011     Software Foundation; either version 2.1 of the License, or (at your option)
    012     any later version.
    013     This library is distributed in the hope that it will be useful, but WITHOUT
    014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
    016     details.
    017     You should have received a copy of the GNU Lesser General Public License
    018     along with this library; if not, write to the Free Software Foundation, Inc.,
    019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
    020
    021     Contact information:
    022
    023     lat/lon GmbH
    024     Aennchenstr. 19, 53177 Bonn
    025     Germany
    026     http://lat-lon.de/
    027
    028     Department of Geography, University of Bonn
    029     Prof. Dr. Klaus Greve
    030     Postfach 1147, 53001 Bonn
    031     Germany
    032     http://www.geographie.uni-bonn.de/deegree/
    033
    034     e-mail: info@deegree.org
    035    ----------------------------------------------------------------------------*/
    036    package org.deegree.model.feature;
    037
    038    import static org.deegree.framework.util.CollectionUtils.map;
    039
    040    import java.net.URL;
    041    import java.util.Date;
    042    import java.util.HashSet;
    043    import java.util.Map;
    044    import java.util.Set;
    045
    046    import org.deegree.datatypes.QualifiedName;
    047    import org.deegree.datatypes.Types;
    048    import org.deegree.datatypes.UnknownTypeException;
    049    import org.deegree.framework.log.ILogger;
    050    import org.deegree.framework.log.LoggerFactory;
    051    import org.deegree.framework.util.TimeTools;
    052    import org.deegree.framework.util.CollectionUtils.Mapper;
    053    import org.deegree.io.datastore.schema.MappedFeatureType;
    054    import org.deegree.model.feature.schema.FeaturePropertyType;
    055    import org.deegree.model.feature.schema.FeatureType;
    056    import org.deegree.model.feature.schema.GeometryPropertyType;
    057    import org.deegree.model.feature.schema.MultiGeometryPropertyType;
    058    import org.deegree.model.feature.schema.PropertyType;
    059    import org.deegree.model.feature.schema.SimplePropertyType;
    060    import org.deegree.model.spatialschema.Geometry;
    061    import org.deegree.model.spatialschema.GeometryException;
    062    import org.deegree.model.spatialschema.JTSAdapter;
    063    import org.deegree.model.spatialschema.MultiSurface;
    064    import org.deegree.model.spatialschema.Surface;
    065    import org.deegree.model.spatialschema.SurfacePatch;
    066    import org.deegree.ogcbase.CommonNamespaces;
    067    import org.deegree.ogcwebservices.OGCWebServiceException;
    068
    069    import com.vividsolutions.jts.algorithm.CGAlgorithms;
    070    import com.vividsolutions.jts.geom.Coordinate;
    071    import com.vividsolutions.jts.geom.CoordinateArrays;
    072    import com.vividsolutions.jts.geom.GeometryFactory;
    073    import com.vividsolutions.jts.geom.LinearRing;
    074    import com.vividsolutions.jts.geom.MultiPolygon;
    075    import com.vividsolutions.jts.geom.Polygon;
    076    import com.vividsolutions.jts.geom.impl.CoordinateArraySequenceFactory;
    077
    078    /**
    079     * Validator for feature instance (that have been constructed without schema information).
    080     * <p>
    081     * Validated features are assigned their respective feature types after successful validation.
    082     *
    083     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
    084     * @author last edited by: $Author: mschneider $
    085     *
    086     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
    087     */
    088    public class Validator {
    089
    090        private static final ILogger LOG = LoggerFactory.getLogger( Validator.class );
    091
    092        private Set<Feature> inValidation = new HashSet<Feature>();
    093
    094        private Map<QualifiedName, FeatureType> ftMap;
    095
    096        /**
    097         * Constructs a new instance of <code>Validator</code> that will use the given map to lookup feature types by their
    098         * names.
    099         *
    100         * @param ftMap
    101         */
    102        public Validator( Map<QualifiedName, FeatureType> ftMap ) {
    103            this.ftMap = ftMap;
    104        }
    105
    106        /**
    107         * Validates the given feature instance (and its subfeatures).
    108         * <p>
    109         * The feature instance is then assigned the corresponding {@link MappedFeatureType}. This also applies to its
    110         * subfeatures.
    111         *
    112         * @param feature
    113         *            feature instance to be validated
    114         * @throws OGCWebServiceException
    115         */
    116        public void validate( Feature feature )
    117                                throws OGCWebServiceException {
    118
    119            if ( inValidation.contains( feature ) ) {
    120                return;
    121            }
    122            inValidation.add( feature );
    123
    124            QualifiedName ftName = feature.getName();
    125            FeatureType ft = this.ftMap.get( ftName );
    126            if ( ft == null ) {
    127                String msg = Messages.format( "ERROR_FT_UNKNOWN", feature.getId(), ftName );
    128                throw new OGCWebServiceException( this.getClass().getName(), msg );
    129            }
    130
    131            int idx = 0;
    132            FeatureProperty[] properties = feature.getProperties();
    133
    134            // remove GML properties if they are not defined for the feature type
    135            Set<QualifiedName> deleteGMLProps = new HashSet<QualifiedName>();
    136            for ( FeatureProperty featureProperty : properties ) {
    137                QualifiedName propName = featureProperty.getName();
    138                // GML namespace property?
    139                if ( CommonNamespaces.GMLNS.equals( propName.getNamespace() ) ) {
    140                    // not defined in the feature type
    141                    if ( ft.getProperty( propName ) == null ) {
    142
    143                        deleteGMLProps.add( propName );
    144                    }
    145                }
    146            }
    147            for ( QualifiedName propName : deleteGMLProps ) {
    148                LOG.logDebug( "Removing property '" + propName + "'." );
    149                feature.removeProperty( propName );
    150            }
    151            if ( deleteGMLProps.size() > 0 ) {
    152                properties = feature.getProperties();
    153            }
    154
    155            PropertyType[] propertyTypes = ft.getProperties();
    156
    157            if ( LOG.isDebug() ) {
    158                LOG.logDebug( "Validating properties", map( properties, new Mapper<QualifiedName, FeatureProperty>() {
    159                    public QualifiedName apply( FeatureProperty u ) {
    160                        return u.getName();
    161                    }
    162
    163                } ) );
    164                LOG.logDebug( "Have property types", map( propertyTypes, new Mapper<QualifiedName, PropertyType>() {
    165                    public QualifiedName apply( PropertyType u ) {
    166                        return u.getName();
    167                    }
    168                } ) );
    169            }
    170
    171            for ( int i = 0; i < propertyTypes.length; i++ ) {
    172                idx += validateProperties( feature, propertyTypes[i], properties, idx );
    173            }
    174            if ( idx != properties.length ) {
    175                String msg = Messages.format( "ERROR_FT_INVALID1", feature.getId(), ftName, properties[idx].getName() );
    176                throw new OGCWebServiceException( this.getClass().getName(), msg );
    177            }
    178
    179            feature.setFeatureType( ft );
    180        }
    181
    182        /**
    183         * Validates that there is the correct amount of properties with the expected type in the given array of properties.
    184         *
    185         * @param feature
    186         * @param propertyType
    187         * @param properties
    188         * @param idx
    189         * @throws OGCWebServiceException
    190         */
    191        private int validateProperties( Feature feature, PropertyType propertyType, FeatureProperty[] properties, int idx )
    192                                throws OGCWebServiceException {
    193            int minOccurs = propertyType.getMinOccurs();
    194            int maxOccurs = propertyType.getMaxOccurs();
    195            QualifiedName propertyName = propertyType.getName();
    196            int count = 0;
    197
    198            while ( idx + count < properties.length ) {
    199                if ( properties[idx + count].getName().equals( propertyName ) ) {
    200                    validate( feature, properties[idx + count], propertyType );
    201                    count++;
    202                } else {
    203                    break;
    204                }
    205            }
    206            if ( count < minOccurs ) {
    207                if ( count == 0 ) {
    208                    String msg = Messages.format( "ERROR_FT_INVALID2", feature.getId(), feature.getName(), propertyName );
    209                    throw new OGCWebServiceException( this.getClass().getName(), msg );
    210                }
    211                String msg = Messages.format( "ERROR_FT_INVALID3", feature.getId(), feature.getName(), propertyName,
    212                                              minOccurs, count );
    213                throw new OGCWebServiceException( this.getClass().getName(), msg );
    214
    215            }
    216            if ( maxOccurs != -1 && count > maxOccurs ) {
    217                String msg = Messages.format( "ERROR_FT_INVALID4", feature.getId(), feature.getName(), propertyName,
    218                                              maxOccurs, count );
    219                throw new OGCWebServiceException( this.getClass().getName(), msg );
    220            }
    221            return count;
    222        }
    223
    224        /**
    225         * Validates that there is the correct amount of properties with the expected type in the given array of properties.
    226         *
    227         * @param feature
    228         * @param property
    229         * @param pt
    230         * @throws OGCWebServiceException
    231         */
    232        private void validate( Feature feature, FeatureProperty property, PropertyType pt )
    233                                throws OGCWebServiceException {
    234
    235            Object value = property.getValue();
    236            if ( pt instanceof SimplePropertyType ) {
    237                if ( pt.getType() != Types.ANYTYPE ) {
    238                    String s = value.toString();
    239                    if ( value instanceof Date ) {
    240                        s = TimeTools.getISOFormattedTime( (Date) value );
    241                    }
    242                    Object newValue = validateSimpleProperty( feature, (SimplePropertyType) pt, s );
    243                    property.setValue( newValue );
    244                }
    245            } else if ( pt instanceof GeometryPropertyType ) {
    246                if ( !( value instanceof Geometry ) ) {
    247                    String msg = Messages.format( "ERROR_WRONG_PROPERTY_TYPE", pt.getName(), feature.getId(),
    248                                                  "GeometryProperty", value.getClass().getName() );
    249                    throw new OGCWebServiceException( this.getClass().getName(), msg );
    250                }
    251                if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
    252                    // Geometry correctedGeometry =
    253                    validateGeometryProperty( feature, (GeometryPropertyType) pt, (Geometry) value );
    254                    // property.setValue( correctedGeometry );
    255                }
    256            } else if ( pt instanceof FeaturePropertyType ) {
    257                if ( !( ( value instanceof Feature ) || ( value instanceof URL ) ) ) {
    258                    String msg = Messages.format( "ERROR_WRONG_PROPERTY_TYPE", pt.getName(), feature.getId(),
    259                                                  "FeatureProperty", value.getClass().getName() );
    260                    throw new OGCWebServiceException( this.getClass().getName(), msg );
    261                }
    262                // only validate subfeature if it's not an external reference
    263                if ( value instanceof Feature ) {
    264                    Feature subfeature = (Feature) value;
    265                    // FeaturePropertyContent content = (FeaturePropertyContent) propertyType.getContents()
    266                    // [0];
    267                    // MappedFeatureType contentFT = content.getFeatureTypeReference().getFeatureType();
    268
    269                    // TODO: check that feature is a correct subsitution for the expected featuretype
    270
    271                    validate( subfeature );
    272                }
    273            } else if ( pt instanceof MultiGeometryPropertyType ) {
    274                throw new OGCWebServiceException( "Handling of MultiGeometryPropertyTypes not implemented "
    275                                                  + "in validateProperty()." );
    276            } else {
    277                throw new OGCWebServiceException( "Internal error: Unhandled property type '" + pt.getClass()
    278                                                  + "' encountered while validating property." );
    279            }
    280        }
    281
    282        /**
    283         * Validates that the given string value can be converted to the type of the given {@link SimplePropertyType}.
    284         *
    285         * @param propertyType
    286         * @param s
    287         * @return corresponding <code>Object</code> for the string value
    288         * @throws OGCWebServiceException
    289         */
    290        private Object validateSimpleProperty( Feature feature, SimplePropertyType propertyType, String s )
    291                                throws OGCWebServiceException {
    292
    293            int type = propertyType.getType();
    294            QualifiedName propertyName = propertyType.getName();
    295
    296            Object value = null;
    297            if ( type == Types.NUMERIC || type == Types.DOUBLE ) {
    298                try {
    299                    value = new Double( s );
    300                } catch ( NumberFormatException e ) {
    301                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Double" );
    302                    throw new OGCWebServiceException( msg );
    303                }
    304            } else if ( type == Types.INTEGER ) {
    305                try {
    306                    value = new Integer( s );
    307                } catch ( NumberFormatException e ) {
    308                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Integer" );
    309                    throw new OGCWebServiceException( msg );
    310                }
    311            } else if ( type == Types.DECIMAL || type == Types.FLOAT ) {
    312                try {
    313                    value = new Float( s );
    314                } catch ( NumberFormatException e ) {
    315                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Float" );
    316                    throw new OGCWebServiceException( msg );
    317                }
    318            } else if ( type == Types.BOOLEAN ) {
    319                value = new Boolean( s );
    320            } else if ( type == Types.VARCHAR ) {
    321                value = s;
    322            } else if ( type == Types.DATE || type == Types.TIMESTAMP ) {
    323                try {
    324                    value = TimeTools.createCalendar( s ).getTime();
    325                } catch ( NumberFormatException e ) {
    326                    LOG.logDebug( "Stack trace: ", e );
    327                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Date" );
    328                    throw new OGCWebServiceException( msg );
    329                }
    330            } else {
    331                String typeString = "" + type;
    332                try {
    333                    typeString = Types.getTypeNameForSQLTypeCode( type );
    334                } catch ( UnknownTypeException e ) {
    335                    LOG.logError( "No type name for code: " + type );
    336                }
    337                String msg = Messages.format( "ERROR_UNHANDLED_TYPE", "" + typeString );
    338                LOG.logError( msg );
    339                throw new OGCWebServiceException( msg );
    340            }
    341            return value;
    342        }
    343
    344        private Geometry validateGeometryProperty( Feature feature, GeometryPropertyType pt, Geometry geometry ) {
    345
    346            try {
    347                com.vividsolutions.jts.geom.Geometry jtsGeometry = JTSAdapter.export( geometry );
    348                if ( !jtsGeometry.isValid() ) {
    349                    String msg = Messages.format( "GEOMETRY_NOT_VALID", pt.getName(), feature.getId() );
    350                    LOG.logDebug( msg );
    351                } else if ( geometry instanceof Surface ) {
    352                    geometry = validatePolygonOrientation( feature, pt, (Surface) geometry, (Polygon) jtsGeometry );
    353                } else if ( geometry instanceof MultiSurface ) {
    354                    geometry = validateMultiPolygonOrientation( feature, pt, (MultiSurface) geometry,
    355                                                                (MultiPolygon) jtsGeometry );
    356                }
    357            } catch ( GeometryException e ) {
    358                LOG.logError( e.getMessage(), e );
    359            }
    360            return geometry;
    361        }
    362
    363        /**
    364         * Checks whether the outer boundary of the given {@link Surface} geometry has counter-clockwise orientation and
    365         * that the inner boundaries have clockwise orientation (as specified by ISO 19107 / GML).
    366         * <p>
    367         * Information on invalid orientations is logged.
    368         *
    369         * @param feature
    370         * @param pt
    371         * @param surface
    372         * @param polygon
    373         * @throws GeometryException
    374         */
    375        private Surface validatePolygonOrientation( Feature feature, GeometryPropertyType pt, Surface surface,
    376                                                    Polygon polygon )
    377                                throws GeometryException {
    378            GeometryFactory factory = new GeometryFactory();
    379            CoordinateArraySequenceFactory coordSeqFactory = CoordinateArraySequenceFactory.instance();
    380
    381            Coordinate[] outerCoords = polygon.getExteriorRing().getCoordinates();
    382            if ( !CGAlgorithms.isCCW( outerCoords ) ) {
    383                String msg = Messages.format( "OUTER_RING_NOT_CCW", pt.getName(), feature.getId() );
    384                LOG.logDebug( msg );
    385                CoordinateArrays.reverse( outerCoords );
    386            }
    387            LinearRing shell = new LinearRing( coordSeqFactory.create( outerCoords ), factory );
    388
    389            LinearRing[] holes = new LinearRing[polygon.getNumInteriorRing()];
    390            for ( int i = 0; i < polygon.getNumInteriorRing(); i++ ) {
    391                Coordinate[] innerCoords = polygon.getInteriorRingN( i ).getCoordinates();
    392                if ( CGAlgorithms.isCCW( innerCoords ) ) {
    393                    String msg = Messages.format( "INNER_RING_NOT_CW", i, pt.getName(), feature.getId() );
    394                    LOG.logDebug( msg );
    395                    CoordinateArrays.reverse( innerCoords );
    396                }
    397                holes[i] = new LinearRing( coordSeqFactory.create( innerCoords ), factory );
    398            }
    399            Surface correctedSurface = (Surface) JTSAdapter.wrap( new Polygon( shell, holes, factory ) );
    400            SurfacePatch[] patches = new SurfacePatch[correctedSurface.getNumberOfSurfacePatches()];
    401            for ( int i = 0; i < patches.length; i++ ) {
    402                patches[i] = correctedSurface.getSurfacePatchAt( 0 );
    403            }
    404            return org.deegree.model.spatialschema.GeometryFactory.createSurface( patches, surface.getCoordinateSystem() );
    405        }
    406
    407        /**
    408         * Checks whether the outer boundaries of the given {@link MultiSurface} members have counter-clockwise orientation
    409         * and that the inner boundaries have clockwise orientation (as specified by ISO 19107 / GML).
    410         * <p>
    411         * Information on invalid orientations is logged.
    412         *
    413         * @param feature
    414         * @param pt
    415         * @param multiSurface
    416         * @param multiPolygon
    417         * @throws GeometryException
    418         */
    419        private MultiSurface validateMultiPolygonOrientation( Feature feature, GeometryPropertyType pt,
    420                                                              MultiSurface multiSurface, MultiPolygon multiPolygon )
    421                                throws GeometryException {
    422            Surface[] surfaces = new Surface[multiPolygon.getNumGeometries()];
    423            for ( int i = 0; i < surfaces.length; i++ ) {
    424                surfaces[i] = validatePolygonOrientation( feature, pt, multiSurface.getSurfaceAt( i ),
    425                                                          (Polygon) multiPolygon.getGeometryN( i ) );
    426            }
    427            return org.deegree.model.spatialschema.GeometryFactory.createMultiSurface( surfaces,
    428                                                                                       multiSurface.getCoordinateSystem() );
    429        }
    430    }