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 }