001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_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 }