001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/ogcwebservices/wfs/FeatureDisambiguator.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2006 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: 7897 $, $Date: 2007-08-06 10:11:35 +0200 (Mo, 06 Aug 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    
199            for ( Feature feature : features ) {
200                // only find features "equal" to feature, if feature does not have an id yet
201                if ( feature.getId() == null || "".equals( feature.getId() ) ) {
202                    List<Feature> equalFeatures = new ArrayList<Feature>();
203                    equalFeatures.add( feature );
204    
205                    for ( Feature otherFeature : features ) {
206                        if ( compareFeatures( feature, otherFeature,
207                                              new HashMap<Feature, List<Feature>>() ) ) {
208                            LOG.logDebug( "Found matching features of type: '" + ft.getName() + "'." );
209                            equalFeatures.add( otherFeature );
210                        }
211                    }
212                    assignSameFID( equalFeatures );
213                }
214            }
215    
216            for ( Feature feature : features ) {
217                String fid = feature.getId();
218                if ( this.representerMap.get( fid ) == null ) {
219                    this.representerMap.put( fid, feature );
220                }
221            }
222        }
223    
224        /**
225         * Assigns the same feature id to every feature in the given list of "equal" features.
226         * <p>
227         * If any feature in the list has a feature id assigned to it already, this feature id is used.
228         * If no feature has a feature id, a new feature id (a UUID) is generated.
229         * 
230         * @param equalFeatures
231         * @throws DatastoreException
232         */
233        private void assignSameFID( List<Feature> equalFeatures )
234                                throws DatastoreException {
235    
236            LOG.logDebug( "Found " + equalFeatures.size() + " 'equal' features of type "
237                          + equalFeatures.get( 0 ).getFeatureType().getName() );
238    
239            String fid = null;
240    
241            // check if any feature has a fid already
242            for ( Feature feature : equalFeatures ) {
243                String otherFid = feature.getId();
244                if ( otherFid != null && !otherFid.equals( "" ) ) {
245                    if ( fid != null && !fid.equals( otherFid ) ) {
246                        String msg = Messages.getMessage( "WFS_IDENTICAL_FEATURES",
247                                                          feature.getFeatureType().getName(), fid,
248                                                          otherFid );
249                        throw new DatastoreException( msg );
250                    }
251                    fid = otherFid;
252                }
253            }
254    
255            if ( fid == null ) {
256                fid = UUID.randomUUID().toString();
257                this.representerMap.put( fid, equalFeatures.get( 0 ) );
258            }
259    
260            // assign fid to every "equal" feature
261            for ( Feature feature : equalFeatures ) {
262                feature.setId( fid );
263            }
264        }
265    
266        /**
267         * Determines whether two feature instances are "equal" according to the annotated schema
268         * (deegreewfs:IdentityPart declarations).
269         * 
270         * @param feature1
271         * @param feature2
272         * @param compareMap
273         * @return true, if the two features are "equal", false otherwise
274         */
275        private boolean compareFeatures( Feature feature1, Feature feature2,
276                                         Map<Feature, List<Feature>> compareMap ) {
277    
278            LOG.logDebug( "feature1: " + feature1.getName() + " id=" + feature1.getId() + " hashCode="
279                          + feature1.hashCode() );
280            LOG.logDebug( "feature2: " + feature2.getName() + " id=" + feature2.getId() + " hashCode="
281                          + feature2.hashCode() );
282    
283            // same feature instance? -> equal
284            if ( feature1 == feature2 ) {
285                return true;
286            }
287    
288            // same feature id -> equal / different feature id -> not equal
289            String fid1 = feature1.getId();
290            String fid2 = feature2.getId();
291            if ( fid1 != null && fid2 != null && !"".equals( fid1 ) && !"".equals( fid2 ) ) {
292                return fid1.equals( fid2 );
293            }
294    
295            // different feature types? -> not equal
296            MappedFeatureType ft = (MappedFeatureType) feature1.getFeatureType();
297            if ( feature2.getFeatureType() != ft ) {
298                return false;
299            }
300    
301            // feature id is identity part? -> not equal (unique ids for all anonymous features)
302            if ( ft.getGMLId().isIdentityPart() ) {
303                return false;
304            }
305    
306            // there is already a compareFeatures() call with these features on the stack
307            // -> end recursion
308            List<Feature> features = compareMap.get( feature1 );
309            if ( features == null ) {
310                features = new ArrayList<Feature>();
311                compareMap.put( feature1, features );
312            } else {
313                for ( Feature feature : features ) {
314                    if ( feature2 == feature ) {
315                        return true;
316                    }
317                }
318            }
319            features.add( feature2 );
320    
321            features = compareMap.get( feature2 );
322            if ( features == null ) {
323                features = new ArrayList<Feature>();
324                compareMap.put( feature2, features );
325            } else {
326                for ( Feature feature : features ) {
327                    if ( feature1 == feature ) {
328                        return true;
329                    }
330                }
331            }
332            features.add( feature1 );
333    
334            // check every "relevant" property for equality
335            PropertyType[] properties = ft.getProperties();
336            for ( int i = 0; i < properties.length; i++ ) {
337                MappedPropertyType propertyType = (MappedPropertyType) properties[i];
338                if ( propertyType.isIdentityPart() ) {
339                    if ( !compareProperties( propertyType, feature1, feature2, compareMap ) ) {
340                        LOG.logDebug( "Not equal: values for property '" + propertyType.getName()
341                                      + " do not match." );
342                        return false;
343                    }
344                }
345            }
346            return true;
347        }
348    
349        /**
350         * Determines whether two feature instances have the same content in the specified property.
351         * 
352         * @param propertyType
353         * @param feature1
354         * @param feature2
355         * @param compareMap
356         * @return true, if the properties are "equal", false otherwise
357         */
358        private boolean compareProperties( MappedPropertyType propertyType, Feature feature1,
359                                           Feature feature2, Map<Feature, List<Feature>> compareMap ) {
360    
361            FeatureProperty[] props1 = feature1.getProperties( propertyType.getName() );
362            FeatureProperty[] props2 = feature2.getProperties( propertyType.getName() );
363    
364            if ( props1 != null && props2 != null ) {
365                if ( props1.length != props2.length ) {
366                    return false;
367                }
368                // TODO handle different orders of multi properties
369                for ( int j = 0; j < props1.length; j++ ) {
370                    Object value1 = props1[j].getValue();
371                    Object value2 = props2[j].getValue();
372    
373                    if ( value1 == null && value2 == null ) {
374                        continue;
375                    } else if ( !( value1 != null && value2 != null ) ) {
376                        return false;
377                    }
378    
379                    if ( value1 instanceof Feature ) {
380                        // complex valued property (subfeature)
381                        if ( !( value2 instanceof Feature ) ) {
382                            return false;
383                        }
384                        Feature subfeature1 = (Feature) value1;
385                        Feature subfeature2 = (Feature) value2;
386    
387                        if ( !compareFeatures( subfeature1, subfeature2, compareMap ) ) {
388                            return false;
389                        }
390                    } else {
391                        if ( value1 instanceof Geometry ) {
392                            String msg = "Check for equal geometry properties is not implemented yet. "
393                                         + "Do not set 'identityPart' to true in geometry property "
394                                         + "definitions.";
395                            throw new RuntimeException( msg );
396                        }
397                        // simple valued property
398                        if ( !value1.equals( value2 ) ) {
399                            return false;
400                        }
401                    }
402                }
403            } else if ( !( props1 == null && props2 == null ) ) {
404                return false;
405            }
406            return true;
407        }
408    
409        /**
410         * Checks that there are no root features in the collection that are "equal".
411         * <p>
412         * After disambiguation these features have the same feature id.
413         * 
414         * @throws DatastoreException
415         */
416        private void checkForEqualRootFeatures()
417                                throws DatastoreException {
418            Set<String> rootFIDs = new HashSet<String>();
419            for ( int i = 0; i < fc.size(); i++ ) {
420                String fid = fc.getFeature( i ).getId();
421                if ( rootFIDs.contains( fid ) ) {
422                    String msg = Messages.getMessage( "WFS_SAME_ROOT_FEATURE_ID" );
423                    throw new DatastoreException( msg );
424                }
425                rootFIDs.add( fid );
426            }
427        }
428    
429        /**
430         * Determines the feature type of all features (and subfeatures) in the given feature collection
431         * and builds a lookup map.
432         * 
433         * @param fc
434         * @return lookup map that maps each feature instance to it's feature type
435         */
436        private Map<MappedFeatureType, Set<Feature>> buildFeatureTypeMap( FeatureCollection fc ) {
437            LOG.logDebug( "Building feature map." );
438            Map<MappedFeatureType, Set<Feature>> ftMap = new HashMap<MappedFeatureType, Set<Feature>>();
439            for ( int i = 0; i < fc.size(); i++ ) {
440                addToFeatureTypeMap( fc.getFeature( i ), ftMap );
441            }
442            return ftMap;
443        }
444    
445        /**
446         * Recursively adds the given feature (and it's subfeatures) to the given map. To cope with
447         * cyclic features, the recursion ends if the feature instance is already present in the map.
448         * 
449         * @param feature
450         *            feature instance to add
451         * @param ftMap
452         */
453        private void addToFeatureTypeMap( Feature feature, Map<MappedFeatureType, Set<Feature>> ftMap ) {
454    
455            MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType();
456            Set<Feature> features = ftMap.get( ft );
457            if ( features == null ) {
458                features = new HashSet<Feature>();
459                ftMap.put( ft, features );
460            } else {
461                if ( features.contains( feature ) ) {
462                    return;
463                }
464            }
465            features.add( feature );
466    
467            // recurse into subfeatures
468            FeatureProperty[] properties = feature.getProperties();
469            for ( int i = 0; i < properties.length; i++ ) {
470                Object propertyValue = properties[i].getValue();
471                if ( propertyValue instanceof Feature ) {
472                    Feature subFeature = (Feature) propertyValue;
473                    addToFeatureTypeMap( subFeature, ftMap );
474                }
475            }
476        }
477    
478        /**
479         * Ensures that all features with the same feature id refer to the same feature instance.
480         * <p>
481         * Xlinked content is used for every subfeature that has been encountered already (or is a root
482         * feature of the collection).
483         * <p>
484         * Root features are never replaced, because {@link #assignFIDsAndRepresenters()} ensures that
485         * root features always represent themselves.
486         */
487        private void replaceDuplicateFeatures() {
488    
489            Set<String> xlinkFIDs = new HashSet<String>();
490    
491            // ensure that root features are always referred to by xlink properties
492            for ( int i = 0; i < this.fc.size(); i++ ) {
493                Feature feature = this.fc.getFeature( i );
494                xlinkFIDs.add( feature.getId() );
495            }
496    
497            for ( int i = 0; i < this.fc.size(); i++ ) {
498                Feature feature = this.fc.getFeature( i );
499                replaceDuplicateFeatures( feature, xlinkFIDs );
500            }
501        }
502    
503        /**
504         * Replaces all subfeatures of the given feature instance by their "representers".
505         * <p>
506         * Xlinked content is used for every subfeature that has been encountered already (or that is a
507         * root feature of the collection).
508         * 
509         * @param feature
510         * @param xlinkFIDs
511         */
512        private void replaceDuplicateFeatures( Feature feature, Set<String> xlinkFIDs ) {
513    
514            LOG.logDebug( "Replacing in feature: '" + feature.getName() + "', " + feature.getId() );
515            xlinkFIDs.add( feature.getId() );
516    
517            FeatureProperty[] properties = feature.getProperties();
518            for ( int i = 0; i < properties.length; i++ ) {
519                Object value = properties[i].getValue();
520                if ( value != null && value instanceof Feature ) {
521    
522                    Feature subfeature = (Feature) value;
523                    String fid = subfeature.getId();
524                    subfeature = this.representerMap.get( fid );
525    
526                    FeatureProperty oldProperty = properties[i];
527                    FeatureProperty newProperty = null;
528    
529                    if ( !xlinkFIDs.contains( fid ) ) {
530                        // first occurence in feature collection
531                        LOG.logDebug( "Not-XLink feature property: " + fid );
532                        newProperty = FeatureFactory.createFeatureProperty( oldProperty.getName(),
533                                                                            subfeature );
534                        replaceDuplicateFeatures( subfeature, xlinkFIDs );
535                    } else {
536                        // successive occurence in feature collection (use XLink)
537                        LOG.logDebug( "XLink feature property: " + fid );
538                        newProperty = new XLinkedFeatureProperty( oldProperty.getName(), fid );
539                        newProperty.setValue( subfeature );
540                    }
541                    feature.replaceProperty( oldProperty, newProperty );
542                }
543            }
544        }
545    }