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 }