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    }