001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/ogcwebservices/wfs/FeatureDisambiguator.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.ogcwebservices.wfs;
037    
038    import java.util.ArrayList;
039    import java.util.Collection;
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    import java.util.UUID;
046    
047    import org.deegree.framework.log.ILogger;
048    import org.deegree.framework.log.LoggerFactory;
049    import org.deegree.i18n.Messages;
050    import org.deegree.io.datastore.DatastoreException;
051    import org.deegree.io.datastore.idgenerator.FeatureIdAssigner;
052    import org.deegree.io.datastore.schema.MappedFeatureType;
053    import org.deegree.io.datastore.schema.MappedPropertyType;
054    import org.deegree.model.feature.Feature;
055    import org.deegree.model.feature.FeatureCollection;
056    import org.deegree.model.feature.FeatureFactory;
057    import org.deegree.model.feature.FeatureProperty;
058    import org.deegree.model.feature.XLinkedFeatureProperty;
059    import org.deegree.model.feature.schema.PropertyType;
060    import org.deegree.model.spatialschema.Geometry;
061    
062    /**
063     * Responsible for the normalization of feature collections that are going to be persisted (i.e. inserted into a
064     * {@link org.deegree.io.datastore Datastore}). This is necessary, because it is up to WFS clients whether feature ids
065     * (gml:id attribute) are provided in an insert/update request or not.
066     * <p>
067     * After processing, the resulting feature collection meets the following requirements:
068     * <ul>
069     * <li>Every member feature (and subfeature) has a unique feature id that uniquely identifies it. Note that this id is
070     * only momentarily valid, and that the final feature id used for storing it is generated by the
071     * {@link FeatureIdAssigner} class in a later step.</li>
072     * <li>Features that are equal according to the annotated schema (deegreewfs:IdentityPart declarations) are represented
073     * by the same feature instance.</li>
074     * <li>Complex feature properties use xlink to specify their content if necessary.</li>
075     * <li>Root features in the feature collection never use xlinks.</li>
076     * </ul>
077     *
078     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
079     * @author last edited by: $Author: mschneider $
080     *
081     * @version $Revision: 18195 $, $Date: 2009-06-18 17:55:39 +0200 (Do, 18. Jun 2009) $
082     */
083    class FeatureDisambiguator {
084    
085        private static final ILogger LOG = LoggerFactory.getLogger( FeatureDisambiguator.class );
086    
087        private FeatureCollection fc;
088    
089        private Map<MappedFeatureType, Set<Feature>> ftMap;
090    
091        // key: feature id, value: feature instance (representer for all features with same id)
092        private Map<String, Feature> representerMap = new HashMap<String, Feature>();
093    
094        /**
095         * Creates a new <code>FeatureDisambiguator</code> to disambiguate the given feature collection.
096         *
097         * @param fc
098         *            feature collection to disambiguate
099         */
100        FeatureDisambiguator( FeatureCollection fc ) {
101            this.fc = fc;
102            this.ftMap = buildFeatureTypeMap( fc );
103        }
104    
105        /**
106         * Checks if any anonymous features (without id) are present in the feature collection.
107         *
108         * @return true, if one or more anonymous features are present, false otherwise
109         */
110        boolean checkForAnonymousFeatures() {
111            for ( MappedFeatureType ft : this.ftMap.keySet() ) {
112                for ( Feature feature : this.ftMap.get( ft ) ) {
113                    if ( feature.getId() == null || feature.getId().equals( "" ) ) {
114                        return true;
115                    }
116                }
117            }
118            return false;
119        }
120    
121        /**
122         * Merges all "equal" feature (and subfeature) instances in the associated feature collection that do not have a
123         * feature id. Afterwards, every feature (and subfeature) in the collection has a unique feature id.
124         * <p>
125         * It is considered an error if there is more than root feature with the same id after the identification of equal
126         * features.
127         * <p>
128         * Furthermore, there is always only one feature instance with a certain id, i.e. "equal" features are represented
129         * by the same feature instance.
130         *
131         * @return "merged" feature collection
132         * @throws DatastoreException
133         */
134        FeatureCollection mergeFeatures()
135                                throws DatastoreException {
136    
137            for ( MappedFeatureType ft : this.ftMap.keySet() ) {
138                LOG.logDebug( ftMap.get( ft ).size() + " features of type: " + ft.getName() );
139            }
140    
141            assignFIDsAndRepresenters();
142            checkForEqualRootFeatures();
143            replaceDuplicateFeatures();
144            return this.fc;
145        }
146    
147        /**
148         * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given type in the feature
149         * collection.
150         * <p>
151         * Also builds the <code>representerMap</code>, so each feature id is mapped to the feature instance that is used as
152         * the single representer for all features instances with this id.
153         * <p>
154         * It is ensured that for each feature id that is associated with a root feature of the collection, the root feature
155         * is used as the representing feature instance. This is important to guarantee that the root features in the
156         * collection represent themselves and need not to be replaced in {@link #replaceDuplicateFeatures()}.
157         *
158         * @throws DatastoreException
159         */
160        private void assignFIDsAndRepresenters()
161                                throws DatastoreException {
162    
163            for ( MappedFeatureType ft : this.ftMap.keySet() ) {
164                assignFIDs( ft );
165            }
166    
167            // ensure that every root feature is the "representer" for it's feature id
168            for ( int i = 0; i < this.fc.size(); i++ ) {
169                Feature rootFeature = this.fc.getFeature( i );
170                String fid = rootFeature.getId();
171                this.representerMap.put( fid, rootFeature );
172            }
173        }
174    
175        /**
176         * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given type in the feature
177         * collection.
178         * <p>
179         * Also builds the <code>representerMap</code>, so every feature id is mapped to a single feature instance that will
180         * represent it everywhere in the collection.
181         *
182         * @param ft
183         * @throws DatastoreException
184         */
185        private void assignFIDs( MappedFeatureType ft )
186                                throws DatastoreException {
187    
188            Collection<Feature> features = this.ftMap.get( ft );
189    
190            LOG.logDebug( "Identifying " + features.size() + " features of type '" + ft.getName() + "'." );
191    
192            for ( Feature feature : features ) {
193                // only find features "equal" to feature, if feature does not have an id yet
194                if ( feature.getId() == null || "".equals( feature.getId() ) ) {
195                    if ( !ft.getGMLId().isIdentityPart() ) {
196                        List<Feature> equalFeatures = new ArrayList<Feature>();
197                        equalFeatures.add( feature );
198    
199                        for ( Feature otherFeature : features ) {
200                            if ( compareFeatures( feature, otherFeature, new HashMap<Feature, List<Feature>>() ) ) {
201                                LOG.logDebug( "Found matching features of type: '" + ft.getName() + "'." );
202                                equalFeatures.add( otherFeature );
203                            }
204                        }
205                        assignSameFID( equalFeatures );
206                    } else {
207                        // don't test for equal features, just assign a new fid
208                        feature.setId( UUID.randomUUID().toString() );
209                    }
210                }
211            }
212    
213            for ( Feature feature : features ) {
214                String fid = feature.getId();
215                if ( this.representerMap.get( fid ) == null ) {
216                    this.representerMap.put( fid, feature );
217                }
218            }
219        }
220    
221        /**
222         * Assigns the same feature id to every feature in the given list of "equal" features.
223         * <p>
224         * If any feature in the list has a feature id assigned to it already, this feature id is used. If no feature has a
225         * feature id, a new feature id (a UUID) is generated.
226         *
227         * @param equalFeatures
228         * @throws DatastoreException
229         */
230        private void assignSameFID( List<Feature> equalFeatures )
231                                throws DatastoreException {
232    
233            LOG.logDebug( "Found " + equalFeatures.size() + " 'equal' features of type "
234                          + equalFeatures.get( 0 ).getFeatureType().getName() );
235    
236            String fid = null;
237    
238            // check if any feature has a fid already
239            for ( Feature feature : equalFeatures ) {
240                String otherFid = feature.getId();
241                if ( otherFid != null && !otherFid.equals( "" ) ) {
242                    if ( fid != null && !fid.equals( otherFid ) ) {
243                        String msg = Messages.getMessage( "WFS_IDENTICAL_FEATURES", feature.getFeatureType().getName(),
244                                                          fid, otherFid );
245                        throw new DatastoreException( msg );
246                    }
247                    fid = otherFid;
248                }
249            }
250    
251            if ( fid == null ) {
252                fid = UUID.randomUUID().toString();
253                this.representerMap.put( fid, equalFeatures.get( 0 ) );
254            }
255    
256            // assign fid to every "equal" feature
257            for ( Feature feature : equalFeatures ) {
258                feature.setId( fid );
259            }
260        }
261    
262        /**
263         * Determines whether two feature instances are "equal" according to the annotated schema (deegreewfs:IdentityPart
264         * declarations).
265         *
266         * @param feature1
267         * @param feature2
268         * @param compareMap
269         * @return true, if the two features are "equal", false otherwise
270         */
271        private boolean compareFeatures( Feature feature1, Feature feature2, Map<Feature, List<Feature>> compareMap ) {
272    
273            LOG.logDebug( "feature1: " + feature1.getName() + " id=" + feature1.getId() + " hashCode="
274                          + feature1.hashCode() );
275            LOG.logDebug( "feature2: " + feature2.getName() + " id=" + feature2.getId() + " hashCode="
276                          + feature2.hashCode() );
277    
278            // same feature instance? -> equal
279            if ( feature1 == feature2 ) {
280                return true;
281            }
282    
283            // same feature id -> equal / different feature id -> not equal
284            String fid1 = feature1.getId();
285            String fid2 = feature2.getId();
286            if ( fid1 != null && fid2 != null && !"".equals( fid1 ) && !"".equals( fid2 ) ) {
287                return fid1.equals( fid2 );
288            }
289    
290            // different feature types? -> not equal
291            MappedFeatureType ft = (MappedFeatureType) feature1.getFeatureType();
292            if ( feature2.getFeatureType() != ft ) {
293                return false;
294            }
295    
296            // feature id is identity part? -> not equal (unique ids for all anonymous features)
297            if ( ft.getGMLId().isIdentityPart() ) {
298                return false;
299            }
300    
301            // there is already a compareFeatures() call with these features on the stack
302            // -> end recursion
303            List<Feature> features = compareMap.get( feature1 );
304            if ( features == null ) {
305                features = new ArrayList<Feature>();
306                compareMap.put( feature1, features );
307            } else {
308                for ( Feature feature : features ) {
309                    if ( feature2 == feature ) {
310                        return true;
311                    }
312                }
313            }
314            features.add( feature2 );
315    
316            features = compareMap.get( feature2 );
317            if ( features == null ) {
318                features = new ArrayList<Feature>();
319                compareMap.put( feature2, features );
320            } else {
321                for ( Feature feature : features ) {
322                    if ( feature1 == feature ) {
323                        return true;
324                    }
325                }
326            }
327            features.add( feature1 );
328    
329            // check every "relevant" property for equality
330            PropertyType[] properties = ft.getProperties();
331            for ( int i = 0; i < properties.length; i++ ) {
332                MappedPropertyType propertyType = (MappedPropertyType) properties[i];
333                if ( propertyType.isIdentityPart() ) {
334                    if ( !compareProperties( propertyType, feature1, feature2, compareMap ) ) {
335                        LOG.logDebug( "Not equal: values for property '" + propertyType.getName() + " do not match." );
336                        return false;
337                    }
338                }
339            }
340            return true;
341        }
342    
343        /**
344         * Determines whether two feature instances have the same content in the specified property.
345         *
346         * @param propertyType
347         * @param feature1
348         * @param feature2
349         * @param compareMap
350         * @return true, if the properties are "equal", false otherwise
351         */
352        private boolean compareProperties( MappedPropertyType propertyType, Feature feature1, Feature feature2,
353                                           Map<Feature, List<Feature>> compareMap ) {
354    
355            FeatureProperty[] props1 = feature1.getProperties( propertyType.getName() );
356            FeatureProperty[] props2 = feature2.getProperties( propertyType.getName() );
357    
358            if ( props1 != null && props2 != null ) {
359                if ( props1.length != props2.length ) {
360                    return false;
361                }
362                // TODO handle different orders of multi properties
363                for ( int j = 0; j < props1.length; j++ ) {
364                    Object value1 = props1[j].getValue();
365                    Object value2 = props2[j].getValue();
366    
367                    if ( value1 == null && value2 == null ) {
368                        continue;
369                    } else if ( !( value1 != null && value2 != null ) ) {
370                        return false;
371                    }
372    
373                    if ( value1 instanceof Feature ) {
374                        // complex valued property (subfeature)
375                        if ( !( value2 instanceof Feature ) ) {
376                            return false;
377                        }
378                        Feature subfeature1 = (Feature) value1;
379                        Feature subfeature2 = (Feature) value2;
380    
381                        if ( !compareFeatures( subfeature1, subfeature2, compareMap ) ) {
382                            return false;
383                        }
384                    } else {
385                        if ( value1 instanceof Geometry ) {
386                            String msg = "Check for equal geometry properties is not implemented yet. "
387                                         + "Do not set 'identityPart' to true in geometry property " + "definitions.";
388                            throw new RuntimeException( msg );
389                        }
390                        // simple valued property
391                        if ( !value1.equals( value2 ) ) {
392                            return false;
393                        }
394                    }
395                }
396            } else if ( !( props1 == null && props2 == null ) ) {
397                return false;
398            }
399            return true;
400        }
401    
402        /**
403         * Checks that there are no root features in the collection that are "equal".
404         * <p>
405         * After disambiguation these features have the same feature id.
406         *
407         * @throws DatastoreException
408         */
409        private void checkForEqualRootFeatures()
410                                throws DatastoreException {
411            Set<String> rootFIDs = new HashSet<String>();
412            for ( int i = 0; i < fc.size(); i++ ) {
413                String fid = fc.getFeature( i ).getId();
414                if ( rootFIDs.contains( fid ) ) {
415                    String msg = Messages.getMessage( "WFS_SAME_ROOT_FEATURE_ID" );
416                    throw new DatastoreException( msg );
417                }
418                rootFIDs.add( fid );
419            }
420        }
421    
422        /**
423         * Determines the feature type of all features (and subfeatures) in the given feature collection and builds a lookup
424         * map.
425         *
426         * @param fc
427         * @return lookup map that maps each feature instance to it's feature type
428         */
429        private Map<MappedFeatureType, Set<Feature>> buildFeatureTypeMap( FeatureCollection fc ) {
430            LOG.logDebug( "Building feature map." );
431            Map<MappedFeatureType, Set<Feature>> ftMap = new HashMap<MappedFeatureType, Set<Feature>>();
432            for ( int i = 0; i < fc.size(); i++ ) {
433                addToFeatureTypeMap( fc.getFeature( i ), ftMap );
434            }
435            return ftMap;
436        }
437    
438        /**
439         * Recursively adds the given feature (and it's subfeatures) to the given map. To cope with cyclic features, the
440         * recursion ends if the feature instance is already present in the map.
441         *
442         * @param feature
443         *            feature instance to add
444         * @param ftMap
445         */
446        private void addToFeatureTypeMap( Feature feature, Map<MappedFeatureType, Set<Feature>> ftMap ) {
447    
448            MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
449            Set<Feature> features = ftMap.get( ft );
450            if ( features == null ) {
451                features = new HashSet<Feature>();
452                ftMap.put( ft, features );
453            } else {
454                if ( features.contains( feature ) ) {
455                    return;
456                }
457            }
458            features.add( feature );
459    
460            // recurse into subfeatures
461            FeatureProperty[] properties = feature.getProperties();
462            for ( int i = 0; i < properties.length; i++ ) {
463                Object propertyValue = properties[i].getValue();
464                if ( propertyValue instanceof Feature ) {
465                    Feature subFeature = (Feature) propertyValue;
466                    addToFeatureTypeMap( subFeature, ftMap );
467                }
468            }
469        }
470    
471        /**
472         * Ensures that all features with the same feature id refer to the same feature instance.
473         * <p>
474         * Xlinked content is used for every subfeature that has been encountered already (or is a root feature of the
475         * collection).
476         * <p>
477         * Root features are never replaced, because {@link #assignFIDsAndRepresenters()} ensures that root features always
478         * represent themselves.
479         */
480        private void replaceDuplicateFeatures() {
481    
482            Set<String> xlinkFIDs = new HashSet<String>();
483    
484            // ensure that root features are always referred to by xlink properties
485            for ( int i = 0; i < this.fc.size(); i++ ) {
486                Feature feature = this.fc.getFeature( i );
487                xlinkFIDs.add( feature.getId() );
488            }
489    
490            for ( int i = 0; i < this.fc.size(); i++ ) {
491                Feature feature = this.fc.getFeature( i );
492                replaceDuplicateFeatures( feature, xlinkFIDs );
493            }
494        }
495    
496        /**
497         * Replaces all subfeatures of the given feature instance by their "representers".
498         * <p>
499         * Xlinked content is used for every subfeature that has been encountered already (or that is a root feature of the
500         * collection).
501         *
502         * @param feature
503         * @param xlinkFIDs
504         */
505        private void replaceDuplicateFeatures( Feature feature, Set<String> xlinkFIDs ) {
506    
507            LOG.logDebug( "Replacing in feature: '" + feature.getName() + "', " + feature.getId() );
508            xlinkFIDs.add( feature.getId() );
509    
510            FeatureProperty[] properties = feature.getProperties();
511            for ( int i = 0; i < properties.length; i++ ) {
512                Object value = properties[i].getValue();
513                if ( value != null && value instanceof Feature ) {
514    
515                    Feature subfeature = (Feature) value;
516                    String fid = subfeature.getId();
517                    subfeature = this.representerMap.get( fid );
518    
519                    FeatureProperty oldProperty = properties[i];
520                    FeatureProperty newProperty = null;
521    
522                    if ( !xlinkFIDs.contains( fid ) ) {
523                        // first occurence in feature collection
524                        LOG.logDebug( "Not-XLink feature property: " + fid );
525                        newProperty = FeatureFactory.createFeatureProperty( oldProperty.getName(), subfeature );
526                        replaceDuplicateFeatures( subfeature, xlinkFIDs );
527                    } else {
528                        // successive occurence in feature collection (use XLink)
529                        LOG.logDebug( "XLink feature property: " + fid );
530                        newProperty = new XLinkedFeatureProperty( oldProperty.getName(), fid );
531                        newProperty.setValue( subfeature );
532                    }
533                    feature.replaceProperty( oldProperty, newProperty );
534                }
535            }
536        }
537    }