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