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