001 //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/src/org/deegree/io/datastore/idgenerator/FeatureIdAssigner.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.io.datastore.idgenerator; 037 038 import java.util.ArrayList; 039 import java.util.Date; 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 046 import org.deegree.datatypes.QualifiedName; 047 import org.deegree.framework.log.ILogger; 048 import org.deegree.framework.log.LoggerFactory; 049 import org.deegree.framework.util.TimeTools; 050 import org.deegree.io.datastore.Datastore; 051 import org.deegree.io.datastore.DatastoreException; 052 import org.deegree.io.datastore.DatastoreTransaction; 053 import org.deegree.io.datastore.FeatureId; 054 import org.deegree.io.datastore.schema.MappedFeatureType; 055 import org.deegree.io.datastore.schema.MappedGMLId; 056 import org.deegree.io.datastore.schema.MappedPropertyType; 057 import org.deegree.io.datastore.schema.MappedSimplePropertyType; 058 import org.deegree.io.datastore.schema.content.MappingField; 059 import org.deegree.io.datastore.schema.content.SimpleContent; 060 import org.deegree.model.crs.UnknownCRSException; 061 import org.deegree.model.feature.Feature; 062 import org.deegree.model.feature.FeatureCollection; 063 import org.deegree.model.feature.FeatureProperty; 064 import org.deegree.model.filterencoding.ComplexFilter; 065 import org.deegree.model.filterencoding.FeatureFilter; 066 import org.deegree.model.filterencoding.Filter; 067 import org.deegree.model.filterencoding.Literal; 068 import org.deegree.model.filterencoding.LogicalOperation; 069 import org.deegree.model.filterencoding.Operation; 070 import org.deegree.model.filterencoding.OperationDefines; 071 import org.deegree.model.filterencoding.PropertyIsCOMPOperation; 072 import org.deegree.model.filterencoding.PropertyName; 073 import org.deegree.model.spatialschema.Geometry; 074 import org.deegree.ogcbase.CommonNamespaces; 075 import org.deegree.ogcbase.PropertyPath; 076 import org.deegree.ogcbase.PropertyPathFactory; 077 import org.deegree.ogcwebservices.wfs.operation.GetFeature; 078 import org.deegree.ogcwebservices.wfs.operation.Query; 079 import org.deegree.ogcwebservices.wfs.operation.transaction.Insert; 080 import org.deegree.ogcwebservices.wfs.operation.transaction.Insert.ID_GEN; 081 082 /** 083 * Responsible for the assigning of valid {@link FeatureId}s which are a prerequisite to the insertion of features in a 084 * {@link Datastore}. For each {@link Insert} operation, a new <code>FeatureIdAssigner</code> instance is created. 085 * <p> 086 * The behaviour of {@link #assignFID(Feature, DatastoreTransaction)}} depends on the {@link ID_GEN} mode in use: 087 * <table> 088 * <tr> 089 * <td>GenerateNew</td> 090 * <td>Prior to the assigning of new feature ids, "equal" features are looked up in the datastore and their feature ids 091 * are used.</td> 092 * </tr> 093 * <tr> 094 * <td>UseExisting</td> 095 * <td> 096 * <ol> 097 * <li>For every root feature, it is checked that a feature id is present and that no feature with the same id already 098 * exists in the datastore.</li> 099 * <li>"Equal" subfeatures are looked up in the datastore and their feature ids are used instead of the given fids -- 100 * if however an "equal" root feature is identified, an exception is thrown.</li> 101 * </ol> 102 * </td> 103 * </tr> 104 * <tr> 105 * <td>ReplaceDuplicate</td> 106 * <td>not supported yet</td> 107 * </tr> 108 * </table> 109 * 110 * @see DatastoreTransaction#performInsert(List) 111 * 112 * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider </a> 113 * @author last edited by: $Author: mschneider $ 114 * 115 * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18 Jun 2009) $ 116 */ 117 public class FeatureIdAssigner { 118 119 /** if an assigned feature id starts with this, it is already stored */ 120 public static final String EXISTS_MARKER = "!"; 121 122 private static final ILogger LOG = LoggerFactory.getLogger( FeatureIdAssigner.class ); 123 124 private ID_GEN idGenMode; 125 126 private Map<String, FeatureId> oldFid2NewFidMap = new HashMap<String, FeatureId>(); 127 128 private Set<Feature> reassignedFeatures = new HashSet<Feature>(); 129 130 private Set<Feature> storedFeatures = new HashSet<Feature>(); 131 132 /** 133 * Creates a new <code>FeatureIdAssigner</code> instance that generates new feature ids as specified. 134 * 135 * @param idGenMode 136 */ 137 public FeatureIdAssigner( ID_GEN idGenMode ) { 138 this.idGenMode = idGenMode; 139 } 140 141 /** 142 * Assigns valid {@link FeatureId}s to the given feature instance and it's subfeatures. 143 * 144 * @param feature 145 * @param ta 146 * @throws IdGenerationException 147 */ 148 public void assignFID( Feature feature, DatastoreTransaction ta ) 149 throws IdGenerationException { 150 151 switch ( this.idGenMode ) { 152 case GENERATE_NEW: { 153 identifyStoredFeatures( feature, ta, new HashSet<Feature>() ); 154 generateAndAssignNewFIDs( feature, null, ta ); 155 break; 156 } 157 case REPLACE_DUPLICATE: { 158 LOG.logInfo( "Idgen mode 'ReplaceDuplicate' is not implemented!" ); 159 break; 160 } 161 case USE_EXISTING: { 162 checkForExistingFid( feature, ta ); 163 String oldFid = feature.getId(); 164 String equalFeature = identifyStoredFeatures( feature, ta, new HashSet<Feature>() ); 165 if ( equalFeature != null ) { 166 String msg = "Cannot perform insert: a feature equal to a feature to be inserted (fid: '" + oldFid 167 + "') already exists in the datastore (existing fid: '" + equalFeature + "')."; 168 throw new IdGenerationException( msg ); 169 } 170 break; 171 } 172 default: { 173 throw new IdGenerationException( "Internal error: Unhandled fid generation mode: " + this.idGenMode ); 174 } 175 } 176 } 177 178 /** 179 * TODO mark stored features a better way 180 */ 181 public void markStoredFeatures() { 182 // hack: mark stored features (with "!") 183 for ( Feature f : this.storedFeatures ) { 184 String fid = f.getId(); 185 if ( !fid.startsWith( EXISTS_MARKER ) ) { 186 f.setId( EXISTS_MARKER + fid ); 187 } 188 } 189 } 190 191 private String identifyStoredFeatures( Feature feature, DatastoreTransaction ta, Set<Feature> inProcessing ) 192 throws IdGenerationException { 193 194 if ( this.reassignedFeatures.contains( feature ) ) { 195 return feature.getId(); 196 } 197 198 inProcessing.add( feature ); 199 200 boolean maybeEqual = true; 201 String existingFID = null; 202 203 LOG.logDebug( "Checking for existing feature that equals feature with type: '" + feature.getName() 204 + "' and fid: '" + feature.getId() + "'." ); 205 206 // build the comparison operations that are needed to select "equal" feature instances 207 List<Operation> compOperations = new ArrayList<Operation>(); 208 209 FeatureProperty[] properties = feature.getProperties(); 210 MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType(); 211 212 for ( int i = 0; i < properties.length; i++ ) { 213 QualifiedName propertyName = properties[i].getName(); 214 MappedPropertyType propertyType = (MappedPropertyType) ft.getProperty( propertyName ); 215 216 Object propertyValue = properties[i].getValue(); 217 if ( propertyValue instanceof Feature ) { 218 219 if ( inProcessing.contains( propertyValue ) ) { 220 LOG.logDebug( "Stopping recursion at property with '" + propertyName + "'. Cycle detected." ); 221 continue; 222 } 223 224 LOG.logDebug( "Recursing on feature property: " + properties[i].getName() ); 225 String subFeatureId = identifyStoredFeatures( (Feature) propertyValue, ta, inProcessing ); 226 if ( propertyType.isIdentityPart() ) { 227 if ( subFeatureId == null ) { 228 maybeEqual = false; 229 } else { 230 LOG.logDebug( "Need to check for feature property '" + propertyName + "' with fid '" 231 + subFeatureId + "'." ); 232 233 // build path that selects subfeature 'gml:id' attribute 234 PropertyPath fidSelectPath = PropertyPathFactory.createPropertyPath( feature.getName() ); 235 fidSelectPath.append( PropertyPathFactory.createPropertyPathStep( propertyName ) ); 236 fidSelectPath.append( PropertyPathFactory.createPropertyPathStep( ( (Feature) propertyValue ).getName() ) ); 237 QualifiedName qn = new QualifiedName( CommonNamespaces.GML_PREFIX, "id", CommonNamespaces.GMLNS ); 238 fidSelectPath.append( PropertyPathFactory.createAttributePropertyPathStep( qn ) ); 239 240 // hack that remove's the gml id prefix 241 MappedFeatureType subFeatureType = (MappedFeatureType) ( (Feature) propertyValue ).getFeatureType(); 242 MappedGMLId gmlId = subFeatureType.getGMLId(); 243 String prefix = gmlId.getPrefix(); 244 if ( subFeatureId.indexOf( prefix ) != 0 ) { 245 throw new IdGenerationException( "Internal error: subfeature id '" + subFeatureId 246 + "' does not begin with the expected prefix." ); 247 } 248 String plainIdValue = subFeatureId.substring( prefix.length() ); 249 PropertyIsCOMPOperation propertyTestOperation = new PropertyIsCOMPOperation( 250 OperationDefines.PROPERTYISEQUALTO, 251 new PropertyName( 252 fidSelectPath ), 253 new Literal( 254 plainIdValue ) ); 255 256 compOperations.add( propertyTestOperation ); 257 } 258 } else 259 LOG.logDebug( "Skipping property '" + propertyName 260 + "': not a part of the feature type's identity." ); 261 } else if ( propertyValue instanceof Geometry ) { 262 263 if ( propertyType.isIdentityPart() ) { 264 throw new IdGenerationException( "Check for equal geometry properties " 265 + "is not implemented yet. Do not set " 266 + "identityPart to true for geometry properties." ); 267 } 268 269 } else { 270 if ( propertyType.isIdentityPart() ) { 271 LOG.logDebug( "Need to check for simple property '" + propertyName + "' with value '" 272 + propertyValue + "'." ); 273 274 String value = propertyValue.toString(); 275 if ( propertyValue instanceof Date ) { 276 value = TimeTools.getISOFormattedTime( (Date) propertyValue ); 277 } 278 279 PropertyIsCOMPOperation propertyTestOperation = new PropertyIsCOMPOperation( 280 OperationDefines.PROPERTYISEQUALTO, 281 new PropertyName( 282 propertyName ), 283 new Literal( value ) ); 284 compOperations.add( propertyTestOperation ); 285 } else { 286 LOG.logDebug( "Skipping property '" + propertyName 287 + "': not a part of the feature type's identity." ); 288 } 289 } 290 } 291 292 if ( ft.getGMLId().isIdentityPart() ) { 293 maybeEqual = false; 294 LOG.logDebug( "Skipping check for identical features: feature id is part of " + "the feature identity." ); 295 } 296 if ( maybeEqual ) { 297 // build the filter from the comparison operations 298 Filter filter = null; 299 if ( compOperations.size() == 0 ) { 300 // no constraints, so any feature of this type will do 301 } else if ( compOperations.size() == 1 ) { 302 filter = new ComplexFilter( compOperations.get( 0 ) ); 303 } else { 304 LogicalOperation andOperation = new LogicalOperation( OperationDefines.AND, compOperations ); 305 filter = new ComplexFilter( andOperation ); 306 } 307 if ( filter != null ) { 308 LOG.logDebug( "Performing query with filter: " + filter.toXML() ); 309 } else { 310 LOG.logDebug( "Performing unrestricted query." ); 311 } 312 Query query = Query.create( new PropertyPath[0], null, null, null, null, 313 new QualifiedName[] { feature.getName() }, null, null, filter, 1, 0, 314 GetFeature.RESULT_TYPE.RESULTS ); 315 316 try { 317 FeatureCollection fc = ft.performQuery( query, ta ); 318 if ( fc.size() > 0 ) { 319 existingFID = fc.getFeature( 0 ).getId(); 320 LOG.logDebug( "Found existing + matching feature with fid: '" + existingFID + "'." ); 321 } else { 322 LOG.logDebug( "No matching feature found." ); 323 } 324 } catch ( DatastoreException e ) { 325 throw new IdGenerationException( "Could not perform query to check for " 326 + "existing feature instances: " + e.getMessage(), e ); 327 } catch ( UnknownCRSException e ) { 328 LOG.logError( e.getMessage(), e ); 329 } 330 } 331 332 if ( existingFID != null ) { 333 LOG.logDebug( "Feature '" + feature.getName() + "', FID '" + feature.getId() + "' -> existing FID '" 334 + existingFID + "'" ); 335 feature.setId( existingFID ); 336 this.storedFeatures.add( feature ); 337 this.reassignedFeatures.add( feature ); 338 changeValueForMappedIDProperties( ft, feature ); 339 } 340 341 return existingFID; 342 } 343 344 /** 345 * TODO: remove parentFID hack 346 * 347 * @param feature 348 * @param parentFID 349 * @throws IdGenerationException 350 */ 351 private void generateAndAssignNewFIDs( Feature feature, FeatureId parentFID, DatastoreTransaction ta ) 352 throws IdGenerationException { 353 354 FeatureId newFid = null; 355 MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType(); 356 357 if ( this.reassignedFeatures.contains( feature ) ) { 358 LOG.logDebug( "Skipping feature with fid '" + feature.getId() + "'. Already reassigned." ); 359 return; 360 } 361 362 this.reassignedFeatures.add( feature ); 363 String oldFidValue = feature.getId(); 364 if ( oldFidValue == null || "".equals( oldFidValue ) ) { 365 LOG.logDebug( "Feature has no FID. Assigning a new one." ); 366 } else { 367 newFid = this.oldFid2NewFidMap.get( oldFidValue ); 368 } 369 if ( newFid == null ) { 370 // TODO remove these hacks 371 if ( ft.getGMLId().getIdGenerator() instanceof ParentIDGenerator ) { 372 newFid = new FeatureId( ft, parentFID.getValues() ); 373 } else { 374 newFid = ft.generateFid( ta ); 375 } 376 this.oldFid2NewFidMap.put( oldFidValue, newFid ); 377 } 378 379 LOG.logDebug( "Feature '" + feature.getName() + "', FID '" + oldFidValue + "' -> new FID '" + newFid + "'" ); 380 // TODO use FeatureId, not it's String value 381 feature.setId( newFid.getAsString() ); 382 changeValueForMappedIDProperties( ft, feature ); 383 384 FeatureProperty[] properties = feature.getProperties(); 385 for ( int i = 0; i < properties.length; i++ ) { 386 Object propertyValue = properties[i].getValue(); 387 if ( propertyValue instanceof Feature ) { 388 generateAndAssignNewFIDs( (Feature) propertyValue, newFid, ta ); 389 } 390 } 391 } 392 393 /** 394 * After reassigning a feature id, this method updates all properties of the feature that are mapped to the same 395 * column as the feature id. 396 * 397 * TODO: find a better way to do this 398 * 399 * @param ft 400 * @param feature 401 */ 402 private void changeValueForMappedIDProperties( MappedFeatureType ft, Feature feature ) { 403 // TODO remove this hack as well 404 String pkColumn = ft.getGMLId().getIdFields()[0].getField(); 405 406 FeatureProperty[] properties = feature.getProperties(); 407 for ( int i = 0; i < properties.length; i++ ) { 408 MappedPropertyType propertyType = (MappedPropertyType) ft.getProperty( properties[i].getName() ); 409 if ( propertyType instanceof MappedSimplePropertyType ) { 410 SimpleContent content = ( (MappedSimplePropertyType) propertyType ).getContent(); 411 if ( content.isUpdateable() ) { 412 if ( content instanceof MappingField ) { 413 String column = ( (MappingField) content ).getField(); 414 if ( column.equalsIgnoreCase( pkColumn ) ) { 415 Object fid = null; 416 try { 417 fid = FeatureId.removeFIDPrefix( feature.getId(), ft.getGMLId() ); 418 } catch ( DatastoreException e ) { 419 e.printStackTrace(); 420 } 421 properties[i].setValue( fid ); 422 } 423 } 424 } 425 } 426 } 427 } 428 429 /** 430 * Checks that the {@link Datastore} contains no feature with the same id as the given feature. 431 * 432 * @param feature 433 * @param ta 434 * @throws IdGenerationException 435 */ 436 private void checkForExistingFid( Feature feature, DatastoreTransaction ta ) 437 throws IdGenerationException { 438 439 MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType(); 440 LOG.logDebug( "Checking for existing feature of type: '" + ft.getName() + "' and with fid: '" + feature.getId() 441 + "'." ); 442 443 // build a filter that matches the feature id 444 FeatureFilter filter = new FeatureFilter(); 445 filter.addFeatureId( new org.deegree.model.filterencoding.FeatureId( feature.getId() ) ); 446 Query query = Query.create( new PropertyPath[0], null, null, null, null, new QualifiedName[] { ft.getName() }, 447 null, null, filter, 1, 0, GetFeature.RESULT_TYPE.HITS ); 448 try { 449 FeatureCollection fc = ft.performQuery( query, ta ); 450 int numFeatures = Integer.parseInt( fc.getAttribute( "numberOfFeatures" ) ); 451 if ( numFeatures > 0 ) { 452 LOG.logInfo( "Found existing feature with fid '" + feature.getId() + "'." ); 453 String msg = "Cannot perform insert: a feature with fid '" + feature.getId() 454 + "' already exists in the datastore (and idGen='UseExisting')."; 455 throw new IdGenerationException( msg ); 456 } 457 LOG.logDebug( "No feature with fid '" + feature.getId() + "' found." ); 458 } catch (IdGenerationException e) { 459 throw e; 460 } catch ( DatastoreException e ) { 461 throw new IdGenerationException( "Could not perform query to check for existing feature instance: " 462 + e.getMessage(), e ); 463 } catch ( UnknownCRSException e ) { 464 LOG.logDebug (e.getMessage(), e); 465 } 466 } 467 }