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 }