001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_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    }