001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/model/feature/Validator.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 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.io.datastore.schema.MappedFeatureType;
057    import org.deegree.model.feature.schema.FeaturePropertyType;
058    import org.deegree.model.feature.schema.FeatureType;
059    import org.deegree.model.feature.schema.GeometryPropertyType;
060    import org.deegree.model.feature.schema.MultiGeometryPropertyType;
061    import org.deegree.model.feature.schema.PropertyType;
062    import org.deegree.model.feature.schema.SimplePropertyType;
063    import org.deegree.model.spatialschema.Geometry;
064    import org.deegree.model.spatialschema.GeometryException;
065    import org.deegree.model.spatialschema.JTSAdapter;
066    import org.deegree.model.spatialschema.MultiSurface;
067    import org.deegree.model.spatialschema.Surface;
068    import org.deegree.model.spatialschema.SurfacePatch;
069    import org.deegree.ogcwebservices.OGCWebServiceException;
070    
071    import com.vividsolutions.jts.algorithm.CGAlgorithms;
072    import com.vividsolutions.jts.geom.Coordinate;
073    import com.vividsolutions.jts.geom.CoordinateArrays;
074    import com.vividsolutions.jts.geom.GeometryFactory;
075    import com.vividsolutions.jts.geom.LinearRing;
076    import com.vividsolutions.jts.geom.MultiPolygon;
077    import com.vividsolutions.jts.geom.Polygon;
078    import com.vividsolutions.jts.geom.impl.CoordinateArraySequenceFactory;
079    
080    /**
081     * Validator for feature instance (that have been constructed without schema information).
082     * <p>
083     * Validated features are assigned their respective feature types after successful validation.
084     * 
085     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a>
086     * @author last edited by: $Author: apoth $
087     * 
088     * @version $Revision: 9343 $, $Date: 2007-12-27 14:30:32 +0100 (Do, 27 Dez 2007) $
089     */
090    public class Validator {
091    
092        private static final ILogger LOG = LoggerFactory.getLogger( Validator.class );
093    
094        private Set<Feature> inValidation = new HashSet<Feature>();
095    
096        private Map<QualifiedName, FeatureType> ftMap;
097    
098        /**
099         * Constructs a new instance of <code>Validator</code> that will use the given map to lookup
100         * feature types by their names.
101         * 
102         * @param ftMap
103         */
104        public Validator( Map<QualifiedName, FeatureType> ftMap ) {
105            this.ftMap = ftMap;
106        }
107    
108        /**
109         * Validates the given feature instance (and its subfeatures).
110         * <p>
111         * The feature instance is then assigned the corresponding {@link MappedFeatureType}. This also
112         * applies to its subfeatures.
113         * 
114         * @param feature
115         *            feature instance to be validated
116         * @throws OGCWebServiceException
117         */
118        public void validate( Feature feature )
119                                throws OGCWebServiceException {
120    
121            if ( inValidation.contains( feature ) ) {
122                return;
123            }
124            inValidation.add( feature );
125    
126            QualifiedName ftName = feature.getName();
127            FeatureType ft = this.ftMap.get( ftName );
128            if ( ft == null ) {
129                String msg = Messages.format( "ERROR_FT_UNKNOWN", feature.getId(), ftName );
130                throw new OGCWebServiceException( this.getClass().getName(), msg );
131            }
132    
133            int idx = 0;
134            FeatureProperty[] properties = feature.getProperties();
135    
136            PropertyType[] propertyTypes = ft.getProperties();
137            for ( int i = 0; i < propertyTypes.length; i++ ) {
138                idx += validateProperties( feature, propertyTypes[i], properties, idx );
139            }
140            if ( idx != properties.length ) {
141                String msg = Messages.format( "ERROR_FT_INVALID1", feature.getId(), ftName, properties[idx].getName() );
142                throw new OGCWebServiceException( this.getClass().getName(), msg );
143            }
144    
145            feature.setFeatureType( ft );
146        }
147    
148        /**
149         * Validates that there is the correct amount of properties with the expected type in the given
150         * array of properties.
151         * 
152         * @param feature
153         * @param propertyType
154         * @param properties
155         * @param idx
156         * @throws OGCWebServiceException
157         */
158        private int validateProperties( Feature feature, PropertyType propertyType, FeatureProperty[] properties, int idx )
159                                throws OGCWebServiceException {
160            int minOccurs = propertyType.getMinOccurs();
161            int maxOccurs = propertyType.getMaxOccurs();
162            QualifiedName propertyName = propertyType.getName();
163            int count = 0;
164    
165            while ( idx + count < properties.length ) {
166                if ( properties[idx + count].getName().equals( propertyName ) ) {
167                    validate( feature, properties[idx + count], propertyType );
168                    count++;
169                } else {
170                    break;
171                }
172            }
173            if ( count < minOccurs ) {
174                if ( count == 0 ) {
175                    String msg = Messages.format( "ERROR_FT_INVALID2", feature.getId(), feature.getName(), propertyName );
176                    throw new OGCWebServiceException( this.getClass().getName(), msg );
177                }
178                String msg = Messages.format( "ERROR_FT_INVALID3", feature.getId(), feature.getName(), propertyName,
179                                              minOccurs, count );
180                throw new OGCWebServiceException( this.getClass().getName(), msg );
181    
182            }
183            if ( maxOccurs != -1 && count > maxOccurs ) {
184                String msg = Messages.format( "ERROR_FT_INVALID4", feature.getId(), feature.getName(), propertyName,
185                                              maxOccurs, count );
186                throw new OGCWebServiceException( this.getClass().getName(), msg );
187            }
188            return count;
189        }
190    
191        /**
192         * Validates that there is the correct amount of properties with the expected type in the given
193         * array of properties.
194         * 
195         * @param feature
196         * @param property
197         * @param pt
198         * @throws OGCWebServiceException
199         */
200        private void validate( Feature feature, FeatureProperty property, PropertyType pt )
201                                throws OGCWebServiceException {
202    
203            Object value = property.getValue();
204            if ( pt instanceof SimplePropertyType ) {
205                if ( pt.getType() != Types.ANYTYPE ) {
206                    String s = value.toString();
207                    if ( value instanceof Date ) {
208                        s = TimeTools.getISOFormattedTime( (Date) value );
209                    }
210                    Object newValue = validateSimpleProperty( feature, (SimplePropertyType) pt, s );
211                    property.setValue( newValue );
212                }
213            } else if ( pt instanceof GeometryPropertyType ) {
214                if ( !( value instanceof Geometry ) ) {
215                    String msg = Messages.format( "ERROR_WRONG_PROPERTY_TYPE", feature.getId(), pt.getName(),
216                                                  "GeometryProperty", value.getClass().getName() );
217                    throw new OGCWebServiceException( this.getClass().getName(), msg );
218                }
219                if ( LOG.getLevel() == ILogger.LOG_DEBUG ) {
220                    Geometry correctedGeometry = validateGeometryProperty( feature, (GeometryPropertyType) pt,
221                                                                           (Geometry) value );
222                    // property.setValue( correctedGeometry );
223                }
224            } else if ( pt instanceof FeaturePropertyType ) {
225                if ( !( value instanceof Feature ) ) {
226                    String msg = Messages.format( "ERROR_WRONG_PROPERTY_TYPE", feature.getId(), pt.getName(),
227                                                  "FeatureProperty", value.getClass().getName() );
228                    throw new OGCWebServiceException( this.getClass().getName(), msg );
229                }
230                Feature subfeature = (Feature) value;
231                // FeaturePropertyContent content = (FeaturePropertyContent) propertyType.getContents()
232                // [0];
233                // MappedFeatureType contentFT = content.getFeatureTypeReference().getFeatureType();
234    
235                // TODO: check that feature is a correct subsitution for the expected featuretype
236    
237                validate( subfeature );
238            } else if ( pt instanceof MultiGeometryPropertyType ) {
239                throw new OGCWebServiceException( "Handling of MultiGeometryPropertyTypes not implemented "
240                                                  + "in validateProperty()." );
241            } else {
242                throw new OGCWebServiceException( "Internal error: Unhandled property type '" + pt.getClass()
243                                                  + "' encountered while validating property." );
244            }
245        }
246    
247        /**
248         * Validates that the given string value can be converted to the type of the given
249         * {@link SimplePropertyType}.
250         * 
251         * @param propertyType
252         * @param s
253         * @return corresponding <code>Object</code> for the string value
254         * @throws OGCWebServiceException
255         */
256        private Object validateSimpleProperty( Feature feature, SimplePropertyType propertyType, String s )
257                                throws OGCWebServiceException {
258    
259            int type = propertyType.getType();
260            QualifiedName propertyName = propertyType.getName();
261    
262            Object value = null;
263            if ( type == Types.NUMERIC || type == Types.DOUBLE ) {
264                try {
265                    value = new Double( s );
266                } catch ( NumberFormatException e ) {
267                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Double" );
268                    throw new OGCWebServiceException( msg );
269                }
270            } else if ( type == Types.INTEGER ) {
271                try {
272                    value = new Integer( s );
273                } catch ( NumberFormatException e ) {
274                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Integer" );
275                    throw new OGCWebServiceException( msg );
276                }
277            } else if ( type == Types.DECIMAL || type == Types.FLOAT ) {
278                try {
279                    value = new Float( s );
280                } catch ( NumberFormatException e ) {
281                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Float" );
282                    throw new OGCWebServiceException( msg );
283                }
284            } else if ( type == Types.BOOLEAN ) {
285                value = new Boolean( s );
286            } else if ( type == Types.VARCHAR ) {
287                value = s;
288            } else if ( type == Types.DATE || type == Types.TIMESTAMP ) {
289                try {
290                    value = TimeTools.createCalendar( s ).getTime();
291                } catch ( NumberFormatException e ) {
292                    String msg = Messages.format( "ERROR_CONVERTING_PROPERTY", s, propertyName, feature.getId(), "Date" );
293                    throw new OGCWebServiceException( msg );
294                }
295            } else {
296                String typeString = "" + type;
297                try {
298                    typeString = Types.getTypeNameForSQLTypeCode( type );
299                } catch ( UnknownTypeException e ) {
300                    LOG.logError( "No type name for code: " + type );
301                }
302                String msg = Messages.format( "ERROR_UNHANDLED_TYPE", "" + typeString );
303                LOG.logError( msg );
304                throw new OGCWebServiceException( msg );
305            }
306            return value;
307        }
308    
309        private Geometry validateGeometryProperty( Feature feature, GeometryPropertyType pt, Geometry geometry ) {
310    
311            try {
312                com.vividsolutions.jts.geom.Geometry jtsGeometry = JTSAdapter.export( geometry );
313                if ( !jtsGeometry.isValid() ) {
314                    String msg = Messages.format( "GEOMETRY_NOT_VALID", pt.getName(), feature.getId() );
315                    LOG.logDebug( msg );
316                } else if ( geometry instanceof Surface ) {
317                    geometry = validatePolygonOrientation( feature, pt, (Surface) geometry, (Polygon) jtsGeometry );
318                } else if ( geometry instanceof MultiSurface ) {
319                    geometry = validateMultiPolygonOrientation( feature, pt, (MultiSurface) geometry,
320                                                                (MultiPolygon) jtsGeometry );
321                }
322            } catch ( GeometryException e ) {
323                LOG.logError( e.getMessage(), e );
324            }
325            return geometry;
326        }
327    
328        /**
329         * Checks whether the outer boundary of the given {@link Surface} geometry has counter-clockwise
330         * orientation and that the inner boundaries have clockwise orientation (as specified by ISO
331         * 19107 / GML).
332         * <p>
333         * Information on invalid orientations is logged.
334         * 
335         * @param feature
336         * @param pt
337         * @param surface
338         * @param polygon
339         * @throws GeometryException
340         */
341        private Surface validatePolygonOrientation( Feature feature, GeometryPropertyType pt, Surface surface,
342                                                    Polygon polygon )
343                                throws GeometryException {
344            GeometryFactory factory = new GeometryFactory();
345            CoordinateArraySequenceFactory coordSeqFactory = CoordinateArraySequenceFactory.instance();
346    
347            Coordinate[] outerCoords = polygon.getExteriorRing().getCoordinates();
348            if ( !CGAlgorithms.isCCW( outerCoords ) ) {
349                String msg = Messages.format( "OUTER_RING_NOT_CCW", pt.getName(), feature.getId() );
350                LOG.logDebug( msg );
351                CoordinateArrays.reverse( outerCoords );
352            }
353            LinearRing shell = new LinearRing( coordSeqFactory.create( outerCoords ), factory );
354    
355            LinearRing[] holes = new LinearRing[polygon.getNumInteriorRing()];
356            for ( int i = 0; i < polygon.getNumInteriorRing(); i++ ) {
357                Coordinate[] innerCoords = polygon.getInteriorRingN( i ).getCoordinates();
358                if ( CGAlgorithms.isCCW( innerCoords ) ) {
359                    String msg = Messages.format( "INNER_RING_NOT_CW", i, pt.getName(), feature.getId() );
360                    LOG.logDebug( msg );
361                    CoordinateArrays.reverse( innerCoords );
362                }
363                holes[i] = new LinearRing( coordSeqFactory.create( innerCoords ), factory );
364            }
365            Surface correctedSurface = (Surface) JTSAdapter.wrap( new Polygon( shell, holes, factory ) );
366            SurfacePatch[] patches = new SurfacePatch[correctedSurface.getNumberOfSurfacePatches()];
367            for ( int i = 0; i < patches.length; i++ ) {
368                patches[i] = correctedSurface.getSurfacePatchAt( 0 );
369            }
370            return org.deegree.model.spatialschema.GeometryFactory.createSurface( patches, surface.getCoordinateSystem() );
371        }
372    
373        /**
374         * Checks whether the outer boundaries of the given {@link MultiSurface} members have
375         * counter-clockwise orientation and that the inner boundaries have clockwise orientation (as
376         * specified by ISO 19107 / GML).
377         * <p>
378         * Information on invalid orientations is logged.
379         * 
380         * @param feature
381         * @param pt
382         * @param multiSurface
383         * @param multiPolygon
384         * @throws GeometryException
385         */
386        private MultiSurface validateMultiPolygonOrientation( Feature feature, GeometryPropertyType pt,
387                                                              MultiSurface multiSurface, MultiPolygon multiPolygon )
388                                throws GeometryException {
389            Surface[] surfaces = new Surface[multiPolygon.getNumGeometries()];
390            for ( int i = 0; i < surfaces.length; i++ ) {
391                surfaces[i] = validatePolygonOrientation( feature, pt, multiSurface.getSurfaceAt( i ),
392                                                          (Polygon) multiPolygon.getGeometryN( i ) );
393            }
394            return org.deegree.model.spatialschema.GeometryFactory.createMultiSurface( surfaces,
395                                                                                       multiSurface.getCoordinateSystem() );
396        }
397    }