001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/src/org/deegree/ogcwebservices/wfs/FeatureDisambiguator.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.ogcwebservices.wfs; 037 038 import java.util.ArrayList; 039 import java.util.Collection; 040 import java.util.HashMap; 041 import java.util.HashSet; 042 import java.util.List; 043 import java.util.Map; 044 import java.util.Set; 045 import java.util.UUID; 046 047 import org.deegree.framework.log.ILogger; 048 import org.deegree.framework.log.LoggerFactory; 049 import org.deegree.i18n.Messages; 050 import org.deegree.io.datastore.DatastoreException; 051 import org.deegree.io.datastore.idgenerator.FeatureIdAssigner; 052 import org.deegree.io.datastore.schema.MappedFeatureType; 053 import org.deegree.io.datastore.schema.MappedPropertyType; 054 import org.deegree.model.feature.Feature; 055 import org.deegree.model.feature.FeatureCollection; 056 import org.deegree.model.feature.FeatureFactory; 057 import org.deegree.model.feature.FeatureProperty; 058 import org.deegree.model.feature.XLinkedFeatureProperty; 059 import org.deegree.model.feature.schema.PropertyType; 060 import org.deegree.model.spatialschema.Geometry; 061 062 /** 063 * Responsible for the normalization of feature collections that are going to be persisted (i.e. inserted into a 064 * {@link org.deegree.io.datastore Datastore}). This is necessary, because it is up to WFS clients whether feature ids 065 * (gml:id attribute) are provided in an insert/update request or not. 066 * <p> 067 * After processing, the resulting feature collection meets the following requirements: 068 * <ul> 069 * <li>Every member feature (and subfeature) has a unique feature id that uniquely identifies it. Note that this id is 070 * only momentarily valid, and that the final feature id used for storing it is generated by the 071 * {@link FeatureIdAssigner} class in a later step.</li> 072 * <li>Features that are equal according to the annotated schema (deegreewfs:IdentityPart declarations) are represented 073 * by the same feature instance.</li> 074 * <li>Complex feature properties use xlink to specify their content if necessary.</li> 075 * <li>Root features in the feature collection never use xlinks.</li> 076 * </ul> 077 * 078 * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a> 079 * @author last edited by: $Author: mschneider $ 080 * 081 * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18 Jun 2009) $ 082 */ 083 class FeatureDisambiguator { 084 085 private static final ILogger LOG = LoggerFactory.getLogger( FeatureDisambiguator.class ); 086 087 private FeatureCollection fc; 088 089 private Map<MappedFeatureType, Set<Feature>> ftMap; 090 091 // key: feature id, value: feature instance (representer for all features with same id) 092 private Map<String, Feature> representerMap = new HashMap<String, Feature>(); 093 094 /** 095 * Creates a new <code>FeatureDisambiguator</code> to disambiguate the given feature collection. 096 * 097 * @param fc 098 * feature collection to disambiguate 099 */ 100 FeatureDisambiguator( FeatureCollection fc ) { 101 this.fc = fc; 102 this.ftMap = buildFeatureTypeMap( fc ); 103 } 104 105 /** 106 * Checks if any anonymous features (without id) are present in the feature collection. 107 * 108 * @return true, if one or more anonymous features are present, false otherwise 109 */ 110 boolean checkForAnonymousFeatures() { 111 for ( MappedFeatureType ft : this.ftMap.keySet() ) { 112 for ( Feature feature : this.ftMap.get( ft ) ) { 113 if ( feature.getId() == null || feature.getId().equals( "" ) ) { 114 return true; 115 } 116 } 117 } 118 return false; 119 } 120 121 /** 122 * Merges all "equal" feature (and subfeature) instances in the associated feature collection that do not have a 123 * feature id. Afterwards, every feature (and subfeature) in the collection has a unique feature id. 124 * <p> 125 * It is considered an error if there is more than root feature with the same id after the identification of equal 126 * features. 127 * <p> 128 * Furthermore, there is always only one feature instance with a certain id, i.e. "equal" features are represented 129 * by the same feature instance. 130 * 131 * @return "merged" feature collection 132 * @throws DatastoreException 133 */ 134 FeatureCollection mergeFeatures() 135 throws DatastoreException { 136 137 for ( MappedFeatureType ft : this.ftMap.keySet() ) { 138 LOG.logDebug( ftMap.get( ft ).size() + " features of type: " + ft.getName() ); 139 } 140 141 assignFIDsAndRepresenters(); 142 checkForEqualRootFeatures(); 143 replaceDuplicateFeatures(); 144 return this.fc; 145 } 146 147 /** 148 * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given type in the feature 149 * collection. 150 * <p> 151 * Also builds the <code>representerMap</code>, so each feature id is mapped to the feature instance that is used as 152 * the single representer for all features instances with this id. 153 * <p> 154 * It is ensured that for each feature id that is associated with a root feature of the collection, the root feature 155 * is used as the representing feature instance. This is important to guarantee that the root features in the 156 * collection represent themselves and need not to be replaced in {@link #replaceDuplicateFeatures()}. 157 * 158 * @throws DatastoreException 159 */ 160 private void assignFIDsAndRepresenters() 161 throws DatastoreException { 162 163 for ( MappedFeatureType ft : this.ftMap.keySet() ) { 164 assignFIDs( ft ); 165 } 166 167 // ensure that every root feature is the "representer" for it's feature id 168 for ( int i = 0; i < this.fc.size(); i++ ) { 169 Feature rootFeature = this.fc.getFeature( i ); 170 String fid = rootFeature.getId(); 171 this.representerMap.put( fid, rootFeature ); 172 } 173 } 174 175 /** 176 * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given type in the feature 177 * collection. 178 * <p> 179 * Also builds the <code>representerMap</code>, so every feature id is mapped to a single feature instance that will 180 * represent it everywhere in the collection. 181 * 182 * @param ft 183 * @throws DatastoreException 184 */ 185 private void assignFIDs( MappedFeatureType ft ) 186 throws DatastoreException { 187 188 Collection<Feature> features = this.ftMap.get( ft ); 189 190 LOG.logDebug( "Identifying " + features.size() + " features of type '" + ft.getName() + "'." ); 191 192 for ( Feature feature : features ) { 193 // only find features "equal" to feature, if feature does not have an id yet 194 if ( feature.getId() == null || "".equals( feature.getId() ) ) { 195 if ( !ft.getGMLId().isIdentityPart() ) { 196 List<Feature> equalFeatures = new ArrayList<Feature>(); 197 equalFeatures.add( feature ); 198 199 for ( Feature otherFeature : features ) { 200 if ( compareFeatures( feature, otherFeature, new HashMap<Feature, List<Feature>>() ) ) { 201 LOG.logDebug( "Found matching features of type: '" + ft.getName() + "'." ); 202 equalFeatures.add( otherFeature ); 203 } 204 } 205 assignSameFID( equalFeatures ); 206 } else { 207 // don't test for equal features, just assign a new fid 208 feature.setId( UUID.randomUUID().toString() ); 209 } 210 } 211 } 212 213 for ( Feature feature : features ) { 214 String fid = feature.getId(); 215 if ( this.representerMap.get( fid ) == null ) { 216 this.representerMap.put( fid, feature ); 217 } 218 } 219 } 220 221 /** 222 * Assigns the same feature id to every feature in the given list of "equal" features. 223 * <p> 224 * If any feature in the list has a feature id assigned to it already, this feature id is used. If no feature has a 225 * feature id, a new feature id (a UUID) is generated. 226 * 227 * @param equalFeatures 228 * @throws DatastoreException 229 */ 230 private void assignSameFID( List<Feature> equalFeatures ) 231 throws DatastoreException { 232 233 LOG.logDebug( "Found " + equalFeatures.size() + " 'equal' features of type " 234 + equalFeatures.get( 0 ).getFeatureType().getName() ); 235 236 String fid = null; 237 238 // check if any feature has a fid already 239 for ( Feature feature : equalFeatures ) { 240 String otherFid = feature.getId(); 241 if ( otherFid != null && !otherFid.equals( "" ) ) { 242 if ( fid != null && !fid.equals( otherFid ) ) { 243 String msg = Messages.getMessage( "WFS_IDENTICAL_FEATURES", feature.getFeatureType().getName(), 244 fid, otherFid ); 245 throw new DatastoreException( msg ); 246 } 247 fid = otherFid; 248 } 249 } 250 251 if ( fid == null ) { 252 fid = UUID.randomUUID().toString(); 253 this.representerMap.put( fid, equalFeatures.get( 0 ) ); 254 } 255 256 // assign fid to every "equal" feature 257 for ( Feature feature : equalFeatures ) { 258 feature.setId( fid ); 259 } 260 } 261 262 /** 263 * Determines whether two feature instances are "equal" according to the annotated schema (deegreewfs:IdentityPart 264 * declarations). 265 * 266 * @param feature1 267 * @param feature2 268 * @param compareMap 269 * @return true, if the two features are "equal", false otherwise 270 */ 271 private boolean compareFeatures( Feature feature1, Feature feature2, Map<Feature, List<Feature>> compareMap ) { 272 273 LOG.logDebug( "feature1: " + feature1.getName() + " id=" + feature1.getId() + " hashCode=" 274 + feature1.hashCode() ); 275 LOG.logDebug( "feature2: " + feature2.getName() + " id=" + feature2.getId() + " hashCode=" 276 + feature2.hashCode() ); 277 278 // same feature instance? -> equal 279 if ( feature1 == feature2 ) { 280 return true; 281 } 282 283 // same feature id -> equal / different feature id -> not equal 284 String fid1 = feature1.getId(); 285 String fid2 = feature2.getId(); 286 if ( fid1 != null && fid2 != null && !"".equals( fid1 ) && !"".equals( fid2 ) ) { 287 return fid1.equals( fid2 ); 288 } 289 290 // different feature types? -> not equal 291 MappedFeatureType ft = (MappedFeatureType) feature1.getFeatureType(); 292 if ( feature2.getFeatureType() != ft ) { 293 return false; 294 } 295 296 // feature id is identity part? -> not equal (unique ids for all anonymous features) 297 if ( ft.getGMLId().isIdentityPart() ) { 298 return false; 299 } 300 301 // there is already a compareFeatures() call with these features on the stack 302 // -> end recursion 303 List<Feature> features = compareMap.get( feature1 ); 304 if ( features == null ) { 305 features = new ArrayList<Feature>(); 306 compareMap.put( feature1, features ); 307 } else { 308 for ( Feature feature : features ) { 309 if ( feature2 == feature ) { 310 return true; 311 } 312 } 313 } 314 features.add( feature2 ); 315 316 features = compareMap.get( feature2 ); 317 if ( features == null ) { 318 features = new ArrayList<Feature>(); 319 compareMap.put( feature2, features ); 320 } else { 321 for ( Feature feature : features ) { 322 if ( feature1 == feature ) { 323 return true; 324 } 325 } 326 } 327 features.add( feature1 ); 328 329 // check every "relevant" property for equality 330 PropertyType[] properties = ft.getProperties(); 331 for ( int i = 0; i < properties.length; i++ ) { 332 MappedPropertyType propertyType = (MappedPropertyType) properties[i]; 333 if ( propertyType.isIdentityPart() ) { 334 if ( !compareProperties( propertyType, feature1, feature2, compareMap ) ) { 335 LOG.logDebug( "Not equal: values for property '" + propertyType.getName() + " do not match." ); 336 return false; 337 } 338 } 339 } 340 return true; 341 } 342 343 /** 344 * Determines whether two feature instances have the same content in the specified property. 345 * 346 * @param propertyType 347 * @param feature1 348 * @param feature2 349 * @param compareMap 350 * @return true, if the properties are "equal", false otherwise 351 */ 352 private boolean compareProperties( MappedPropertyType propertyType, Feature feature1, Feature feature2, 353 Map<Feature, List<Feature>> compareMap ) { 354 355 FeatureProperty[] props1 = feature1.getProperties( propertyType.getName() ); 356 FeatureProperty[] props2 = feature2.getProperties( propertyType.getName() ); 357 358 if ( props1 != null && props2 != null ) { 359 if ( props1.length != props2.length ) { 360 return false; 361 } 362 // TODO handle different orders of multi properties 363 for ( int j = 0; j < props1.length; j++ ) { 364 Object value1 = props1[j].getValue(); 365 Object value2 = props2[j].getValue(); 366 367 if ( value1 == null && value2 == null ) { 368 continue; 369 } else if ( !( value1 != null && value2 != null ) ) { 370 return false; 371 } 372 373 if ( value1 instanceof Feature ) { 374 // complex valued property (subfeature) 375 if ( !( value2 instanceof Feature ) ) { 376 return false; 377 } 378 Feature subfeature1 = (Feature) value1; 379 Feature subfeature2 = (Feature) value2; 380 381 if ( !compareFeatures( subfeature1, subfeature2, compareMap ) ) { 382 return false; 383 } 384 } else { 385 if ( value1 instanceof Geometry ) { 386 String msg = "Check for equal geometry properties is not implemented yet. " 387 + "Do not set 'identityPart' to true in geometry property " + "definitions."; 388 throw new RuntimeException( msg ); 389 } 390 // simple valued property 391 if ( !value1.equals( value2 ) ) { 392 return false; 393 } 394 } 395 } 396 } else if ( !( props1 == null && props2 == null ) ) { 397 return false; 398 } 399 return true; 400 } 401 402 /** 403 * Checks that there are no root features in the collection that are "equal". 404 * <p> 405 * After disambiguation these features have the same feature id. 406 * 407 * @throws DatastoreException 408 */ 409 private void checkForEqualRootFeatures() 410 throws DatastoreException { 411 Set<String> rootFIDs = new HashSet<String>(); 412 for ( int i = 0; i < fc.size(); i++ ) { 413 String fid = fc.getFeature( i ).getId(); 414 if ( rootFIDs.contains( fid ) ) { 415 String msg = Messages.getMessage( "WFS_SAME_ROOT_FEATURE_ID" ); 416 throw new DatastoreException( msg ); 417 } 418 rootFIDs.add( fid ); 419 } 420 } 421 422 /** 423 * Determines the feature type of all features (and subfeatures) in the given feature collection and builds a lookup 424 * map. 425 * 426 * @param fc 427 * @return lookup map that maps each feature instance to it's feature type 428 */ 429 private Map<MappedFeatureType, Set<Feature>> buildFeatureTypeMap( FeatureCollection fc ) { 430 LOG.logDebug( "Building feature map." ); 431 Map<MappedFeatureType, Set<Feature>> ftMap = new HashMap<MappedFeatureType, Set<Feature>>(); 432 for ( int i = 0; i < fc.size(); i++ ) { 433 addToFeatureTypeMap( fc.getFeature( i ), ftMap ); 434 } 435 return ftMap; 436 } 437 438 /** 439 * Recursively adds the given feature (and it's subfeatures) to the given map. To cope with cyclic features, the 440 * recursion ends if the feature instance is already present in the map. 441 * 442 * @param feature 443 * feature instance to add 444 * @param ftMap 445 */ 446 private void addToFeatureTypeMap( Feature feature, Map<MappedFeatureType, Set<Feature>> ftMap ) { 447 448 MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType(); 449 Set<Feature> features = ftMap.get( ft ); 450 if ( features == null ) { 451 features = new HashSet<Feature>(); 452 ftMap.put( ft, features ); 453 } else { 454 if ( features.contains( feature ) ) { 455 return; 456 } 457 } 458 features.add( feature ); 459 460 // recurse into subfeatures 461 FeatureProperty[] properties = feature.getProperties(); 462 for ( int i = 0; i < properties.length; i++ ) { 463 Object propertyValue = properties[i].getValue(); 464 if ( propertyValue instanceof Feature ) { 465 Feature subFeature = (Feature) propertyValue; 466 addToFeatureTypeMap( subFeature, ftMap ); 467 } 468 } 469 } 470 471 /** 472 * Ensures that all features with the same feature id refer to the same feature instance. 473 * <p> 474 * Xlinked content is used for every subfeature that has been encountered already (or is a root feature of the 475 * collection). 476 * <p> 477 * Root features are never replaced, because {@link #assignFIDsAndRepresenters()} ensures that root features always 478 * represent themselves. 479 */ 480 private void replaceDuplicateFeatures() { 481 482 Set<String> xlinkFIDs = new HashSet<String>(); 483 484 // ensure that root features are always referred to by xlink properties 485 for ( int i = 0; i < this.fc.size(); i++ ) { 486 Feature feature = this.fc.getFeature( i ); 487 xlinkFIDs.add( feature.getId() ); 488 } 489 490 for ( int i = 0; i < this.fc.size(); i++ ) { 491 Feature feature = this.fc.getFeature( i ); 492 replaceDuplicateFeatures( feature, xlinkFIDs ); 493 } 494 } 495 496 /** 497 * Replaces all subfeatures of the given feature instance by their "representers". 498 * <p> 499 * Xlinked content is used for every subfeature that has been encountered already (or that is a root feature of the 500 * collection). 501 * 502 * @param feature 503 * @param xlinkFIDs 504 */ 505 private void replaceDuplicateFeatures( Feature feature, Set<String> xlinkFIDs ) { 506 507 LOG.logDebug( "Replacing in feature: '" + feature.getName() + "', " + feature.getId() ); 508 xlinkFIDs.add( feature.getId() ); 509 510 FeatureProperty[] properties = feature.getProperties(); 511 for ( int i = 0; i < properties.length; i++ ) { 512 Object value = properties[i].getValue(); 513 if ( value != null && value instanceof Feature ) { 514 515 Feature subfeature = (Feature) value; 516 String fid = subfeature.getId(); 517 subfeature = this.representerMap.get( fid ); 518 519 FeatureProperty oldProperty = properties[i]; 520 FeatureProperty newProperty = null; 521 522 if ( !xlinkFIDs.contains( fid ) ) { 523 // first occurence in feature collection 524 LOG.logDebug( "Not-XLink feature property: " + fid ); 525 newProperty = FeatureFactory.createFeatureProperty( oldProperty.getName(), subfeature ); 526 replaceDuplicateFeatures( subfeature, xlinkFIDs ); 527 } else { 528 // successive occurence in feature collection (use XLink) 529 LOG.logDebug( "XLink feature property: " + fid ); 530 newProperty = new XLinkedFeatureProperty( oldProperty.getName(), fid ); 531 newProperty.setValue( subfeature ); 532 } 533 feature.replaceProperty( oldProperty, newProperty ); 534 } 535 } 536 } 537 }