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