//$Header: /home/deegree/jail/deegreerepository/deegree/src/org/deegree/ogcwebservices/wfs/FeatureDisambiguator.java,v 1.9 2006/11/16 08:53:21 mschneider Exp $ /*---------------- FILE HEADER ------------------------------------------ This file is part of deegree. Copyright (C) 2001-2006 by: Department of Geography, University of Bonn http://www.giub.uni-bonn.de/deegree/ lat/lon GmbH http://www.lat-lon.de This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Contact: Andreas Poth lat/lon GmbH Aennchenstraße 19 53177 Bonn Germany E-Mail: poth@lat-lon.de Prof. Dr. Klaus Greve Department of Geography University of Bonn Meckenheimer Allee 166 53115 Bonn Germany E-Mail: greve@giub.uni-bonn.de ---------------------------------------------------------------------------*/ package org.deegree.ogcwebservices.wfs; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.deegree.framework.log.ILogger; import org.deegree.framework.log.LoggerFactory; import org.deegree.framework.util.BasicUUIDFactory; import org.deegree.i18n.Messages; import org.deegree.io.datastore.DatastoreException; import org.deegree.io.datastore.schema.MappedFeatureType; import org.deegree.io.datastore.schema.MappedPropertyType; import org.deegree.model.feature.Feature; import org.deegree.model.feature.FeatureCollection; import org.deegree.model.feature.FeatureFactory; import org.deegree.model.feature.FeatureProperty; import org.deegree.model.feature.XLinkedFeatureProperty; import org.deegree.model.feature.schema.PropertyType; import org.deegree.model.spatialschema.Geometry; /** * Responsible for the normalization of feature collections that are going to be persisted (i.e. * inserted into a datastore). This is necessary, because it is up to every WFS client whether * feature ids (gml:id attribute) are provided in an insert/update request or not. * <p> * After processing, the resulting feature collection meets the following requirements: * <ul> * <li>Every member feature (and subfeature) has a unique feature id that disambiguates it. Note * that this id is only momentarily valid, and that the final feature id is generated by the * <code>FeatureIdAssigner</code> class in a later step.</li> * <li>Features that are equal according to the annotated schema (deegreewfs:IdentityPart * declarations) are represented by the same feature instance.</li> * <li>Complex feature properties use xlink to specify their content if necessary.</li> * <li>Root features in the feature collection never use xlinks.</li> * </ul> * * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a> * @author last edited by: $Author: mschneider $ * * @version $Revision: 1.9 $, $Date: 2006/11/16 08:53:21 $ */ class FeatureDisambiguator { private static final ILogger LOG = LoggerFactory.getLogger( FeatureDisambiguator.class ); private static BasicUUIDFactory fidFactory = new BasicUUIDFactory(); private FeatureCollection fc; private Map<MappedFeatureType, Set<Feature>> ftMap; // key: feature id, value: feature instance (representer for all features with same id) private Map<String, Feature> representerMap = new HashMap<String, Feature>(); /** * Creates a new <code>FeatureDisambiguator</code> to disambiguate the given feature * collection. * * @param fc feature collection to disambiguate */ FeatureDisambiguator( FeatureCollection fc ) { this.fc = fc; this.ftMap = buildFeatureTypeMap( fc ); } /** * Merges all "equal" feature (and subfeature) instances in the associated feature collection * that do not have a feature id. Afterwards, every feature (and subfeature) in the collection * has a unique feature id. * <p> * It is considered an error if there is more than root feature with the same id after the * identification of equal features. * <p> * Furthermore, there is always only one feature instance with a certain id, i.e. "equal" * features are represented by the same feature instance. * * @return "merged" feature collection * @throws DatastoreException */ public FeatureCollection mergeFeatures() throws DatastoreException { assignFIDsAndRepresenters(); checkForEqualRootFeatures(); replaceDuplicateFeatures(); return this.fc; } /** * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given * type in the feature collection. * <p> * Also builds the <code>representerMap</code>, so each feature id is mapped to the feature * instance that is used as the single representer for all features instances with this id. * <p> * It is ensured that for each feature id that is associated with a root feature of the * collection, the root feature is used as the representing feature instance. This is * important to guarantee that the root features in the collection represent themselves * and need not to be replaced in {@link #replaceDuplicateFeatures()}. * * @throws DatastoreException */ private void assignFIDsAndRepresenters() throws DatastoreException { for ( MappedFeatureType ft : this.ftMap.keySet() ) { assignFIDs( ft ); } // ensure that every root feature is the "representer" for it's feature id for ( int i = 0; i < this.fc.size(); i++ ) { Feature rootFeature = this.fc.getFeature( i ); String fid = rootFeature.getId(); this.representerMap.put( fid, rootFeature ); } } /** * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given * type in the feature collection. * <p> * Also builds the <code>representerMap</code>, so every feature id is mapped to a single * feature instance that will represent it everywhere in the collection. * * @param ft * @throws DatastoreException */ private void assignFIDs( MappedFeatureType ft ) throws DatastoreException { Collection<Feature> features = this.ftMap.get( ft ); LOG.logDebug ("Identifying " + features.size() + " features of type '" + ft.getName() + "'."); for ( Feature feature : features ) { // only find features "equal" to feature, if feature does not have an id yet if ( feature.getId() == null || "".equals( feature.getId() ) ) { List<Feature> equalFeatures = new ArrayList<Feature>(); equalFeatures.add( feature ); for ( Feature otherFeature : features ) { if ( compareFeatures( feature, otherFeature, new HashMap<Feature, List<Feature>>() ) ) { LOG.logDebug( "Found matching features of type: '" + ft.getName() + "'." ); equalFeatures.add( otherFeature ); } } assignSameFID( equalFeatures ); } } for ( Feature feature : features ) { String fid = feature.getId(); if ( this.representerMap.get( fid ) == null ) { this.representerMap.put( fid, feature ); } } } /** * Assigns the same feature id to every feature in the given list of "equal" features. * <p> * If any feature in the list has a feature id assigned to it already, this feature id is used. * If no feature has a feature id, a new feature id (a UUID) is generated. * * @param equalFeatures * @throws DatastoreException */ private void assignSameFID( List<Feature> equalFeatures ) throws DatastoreException { LOG.logDebug( "Found " + equalFeatures.size() + " 'equal' features of type " + equalFeatures.get( 0 ).getFeatureType().getName() ); String fid = null; // check if any feature has a fid already for ( Feature feature : equalFeatures ) { String otherFid = feature.getId(); if ( otherFid != null && !otherFid.equals( "" ) ) { if ( fid != null && !fid.equals( otherFid ) ) { String msg = Messages.getMessage( "WFS_IDENTICAL_FEATURES", feature.getFeatureType().getName(), fid, otherFid ); throw new DatastoreException( msg ); } fid = otherFid; } } if ( fid == null ) { fid = fidFactory.createUUID().toANSIidentifier(); this.representerMap.put( fid, equalFeatures.get( 0 ) ); } // assign fid to every "equal" feature for ( Feature feature : equalFeatures ) { feature.setId( fid ); } } /** * Determines whether two feature instances are "equal" according to the annotated schema * (deegreewfs:IdentityPart declarations). * * @param feature1 * @param feature2 * @param compareMap * @return true, if the two features are "equal", false otherwise */ private boolean compareFeatures( Feature feature1, Feature feature2, Map<Feature, List<Feature>> compareMap ) { LOG.logDebug( "feature1: " + feature1.getName() + " id=" + feature1.getId() + " hashCode=" + feature1.hashCode() ); LOG.logDebug( "feature2: " + feature2.getName() + " id=" + feature2.getId() + " hashCode=" + feature2.hashCode() ); // same feature instance? -> equal if ( feature1 == feature2 ) { return true; } // same feature id -> equal / different feature id -> not equal String fid1 = feature1.getId(); String fid2 = feature2.getId(); if ( fid1 != null && fid2 != null && !"".equals( fid1 ) && !"".equals( fid2 ) ) { return fid1.equals( fid2 ); } // different feature types? -> not equal MappedFeatureType ft = (MappedFeatureType) feature1.getFeatureType(); if ( feature2.getFeatureType() != ft ) { return false; } // feature id is identity part? -> not equal (unique ids for all anonymous features) if ( ft.getGMLId().isIdentityPart() ) { return false; } // there is already a compareFeatures() call with these features on the stack // -> end recursion List<Feature> features = compareMap.get( feature1 ); if ( features == null ) { features = new ArrayList<Feature>(); compareMap.put( feature1, features ); } else { for ( Feature feature : features ) { if ( feature2 == feature ) { return true; } } } features.add( feature2 ); features = compareMap.get( feature2 ); if ( features == null ) { features = new ArrayList<Feature>(); compareMap.put( feature2, features ); } else { for ( Feature feature : features ) { if ( feature1 == feature ) { return true; } } } features.add( feature1 ); // check every "relevant" property for equality PropertyType[] properties = ft.getProperties(); for ( int i = 0; i < properties.length; i++ ) { MappedPropertyType propertyType = (MappedPropertyType) properties[i]; if ( propertyType.isIdentityPart() ) { if ( !compareProperties( propertyType, feature1, feature2, compareMap ) ) { LOG.logDebug( "Not equal: values for property '" + propertyType.getName() + " do not match." ); return false; } } } return true; } /** * Determines whether two feature instances have the same content in the specified property. * * @param propertyType * @param feature1 * @param feature2 * @param compareMap * @return true, if the properties are "equal", false otherwise */ private boolean compareProperties( MappedPropertyType propertyType, Feature feature1, Feature feature2, Map<Feature, List<Feature>> compareMap ) { FeatureProperty[] props1 = feature1.getProperties( propertyType.getName() ); FeatureProperty[] props2 = feature2.getProperties( propertyType.getName() ); if ( props1 != null && props2 != null ) { if ( props1.length != props2.length ) { return false; } // TODO handle different orders of multi properties for ( int j = 0; j < props1.length; j++ ) { Object value1 = props1[j].getValue(); Object value2 = props2[j].getValue(); if ( value1 == null && value2 == null ) { continue; } else if ( !( value1 != null && value2 != null ) ) { return false; } if ( value1 instanceof Feature ) { // complex valued property (subfeature) if ( !( value2 instanceof Feature ) ) { return false; } Feature subfeature1 = (Feature) value1; Feature subfeature2 = (Feature) value2; if ( !compareFeatures( subfeature1, subfeature2, compareMap ) ) { return false; } } else { if ( value1 instanceof Geometry ) { String msg = "Check for equal geometry properties is not implemented yet. " + "Do not set 'identityPart' to true in geometry property " + "definitions."; throw new RuntimeException( msg ); } // simple valued property if ( !value1.equals( value2 ) ) { return false; } } } } else if ( !( props1 == null && props2 == null ) ) { return false; } return true; } /** * Checks that there are no root features in the collection that are "equal". * <p> * After disambiguation these features have the same feature id. * * @throws DatastoreException */ private void checkForEqualRootFeatures() throws DatastoreException { Set<String> rootFIDs = new HashSet<String>(); for ( int i = 0; i < fc.size(); i++ ) { String fid = fc.getFeature(i).getId(); if ( rootFIDs.contains( fid ) ) { String msg = Messages.getMessage( "WFS_SAME_ROOT_FEATURE_ID" ); throw new DatastoreException( msg ); } rootFIDs.add( fid ); } } /** * Determines the feature type of all features (and subfeatures) in the given feature collection * and builds a lookup map. * * @param fc * @return lookup map that maps each feature instance to it's feature type */ private Map<MappedFeatureType, Set<Feature>> buildFeatureTypeMap( FeatureCollection fc ) { LOG.logDebug( "Building feature map." ); Map<MappedFeatureType, Set<Feature>> ftMap = new HashMap<MappedFeatureType, Set<Feature>>(); for ( int i = 0; i < fc.size(); i++ ) { addToFeatureTypeMap( fc.getFeature( i ), ftMap ); } return ftMap; } /** * Recursively adds the given feature (and it's subfeatures) to the given map. To cope with * cyclic features, the recursion ends if the feature instance is already present in the map. * * @param feature * feature instance to add * @param ftMap */ private void addToFeatureTypeMap( Feature feature, Map<MappedFeatureType, Set<Feature>> ftMap ) { MappedFeatureType ft = (MappedFeatureType) feature.getFeatureType(); Set<Feature> features = ftMap.get( ft ); if ( features == null ) { features = new HashSet<Feature>(); ftMap.put( ft, features ); } else { if ( features.contains( feature ) ) { return; } } features.add( feature ); // recurse into subfeatures FeatureProperty[] properties = feature.getProperties(); for ( int i = 0; i < properties.length; i++ ) { Object propertyValue = properties[i].getValue(); if ( propertyValue instanceof Feature ) { Feature subFeature = (Feature) propertyValue; addToFeatureTypeMap( subFeature, ftMap ); } } } /** * Ensures that all features with the same feature id refer to the same feature instance. * <p> * Xlinked content is used for every subfeature that has been encountered already (or is * a root feature of the collection). * <p> * Root features are never replaced, because {@link #assignFIDsAndRepresenters()} ensures * that root features always represent themselves. */ private void replaceDuplicateFeatures() { Set<String> xlinkFIDs = new HashSet<String>(); // ensure that root features are always referred to by xlink properties for ( int i = 0; i < this.fc.size(); i++ ) { Feature feature = this.fc.getFeature( i ); xlinkFIDs.add( feature.getId() ); } for ( int i = 0; i < this.fc.size(); i++ ) { Feature feature = this.fc.getFeature( i ); replaceDuplicateFeatures( feature, xlinkFIDs ); } } /** * Replaces all subfeatures of the given feature instance by their "representers". * <p> * Xlinked content is used for every subfeature that has been encountered already (or that is * a root feature of the collection). * * @param feature * @param xlinkFIDs */ private void replaceDuplicateFeatures( Feature feature, Set<String> xlinkFIDs ) { LOG.logDebug( "Replacing in feature: '" + feature.getName() + "', " + feature.getId() ); xlinkFIDs.add( feature.getId() ); FeatureProperty[] properties = feature.getProperties(); for ( int i = 0; i < properties.length; i++ ) { Object value = properties[i].getValue(); if ( value != null && value instanceof Feature ) { Feature subfeature = (Feature) value; String fid = subfeature.getId(); subfeature = this.representerMap.get( fid ); FeatureProperty oldProperty = properties[i]; FeatureProperty newProperty = null; if ( !xlinkFIDs.contains( fid ) ) { // first occurence in feature collection LOG.logDebug( "Not-XLink feature property: " + fid ); newProperty = FeatureFactory.createFeatureProperty( oldProperty.getName(), subfeature ); replaceDuplicateFeatures( subfeature, xlinkFIDs ); } else { // successive occurence in feature collection (use XLink) LOG.logDebug( "XLink feature property: " + fid ); newProperty = new XLinkedFeatureProperty( oldProperty.getName(), fid ); newProperty.setValue( subfeature ); } feature.replaceProperty( oldProperty, newProperty ); } } } } /* ************************************************************************************************** * Changes to this class. What the people have been up to: * $Log: FeatureDisambiguator.java,v $ * Revision 1.9 2006/11/16 08:53:21 mschneider * Merged messages from org.deegree.ogcwebservices.wfs and its subpackages. * * Revision 1.8 2006/08/14 13:15:54 mschneider * Fixed imports. * * Revision 1.7 2006/08/09 17:04:24 mschneider * Added check for root features with same id (after disambiguation). Improved replacing of features with their representers - root features are always representer for their feature id. * * Revision 1.6 2006/08/09 12:49:18 mschneider * Corrected check for "equal" features with different ids. * * Revision 1.5 2006/08/08 16:18:10 mschneider * Only find "equal" features for anonymous features (that don't have an id). * * Revision 1.4 2006/08/08 16:04:40 mschneider * Improved. * * Revision 1.3 2006/08/01 10:42:06 mschneider * Restructured + cleanup. * * Revision 1.2 2006/07/26 18:57:47 mschneider * Javadoc improvements. * * Revision 1.1 2006/07/25 15:51:19 mschneider * Former functionality of TransactionHandler. * ************************************************************************************************* */