001: //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/trunk/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 08:22:25 -0800 (Thu, 27 Dec 2007) $
089: */
090: class FeatureDisambiguator {
091:
092: private static final ILogger LOG = LoggerFactory
093: .getLogger(FeatureDisambiguator.class);
094:
095: private FeatureCollection fc;
096:
097: private Map<MappedFeatureType, Set<Feature>> ftMap;
098:
099: // key: feature id, value: feature instance (representer for all features with same id)
100: private Map<String, Feature> representerMap = new HashMap<String, Feature>();
101:
102: /**
103: * Creates a new <code>FeatureDisambiguator</code> to disambiguate the given feature
104: * collection.
105: *
106: * @param fc
107: * feature collection to disambiguate
108: */
109: FeatureDisambiguator(FeatureCollection fc) {
110: this .fc = fc;
111: this .ftMap = buildFeatureTypeMap(fc);
112: }
113:
114: /**
115: * Checks if any anonymous features (without id) are present in the feature collection.
116: *
117: * @return true, if one or more anonymous features are present, false otherwise
118: */
119: boolean checkForAnonymousFeatures() {
120: for (MappedFeatureType ft : this .ftMap.keySet()) {
121: for (Feature feature : this .ftMap.get(ft)) {
122: if (feature.getId() == null
123: || feature.getId().equals("")) {
124: return true;
125: }
126: }
127: }
128: return false;
129: }
130:
131: /**
132: * Merges all "equal" feature (and subfeature) instances in the associated feature collection
133: * that do not have a feature id. Afterwards, every feature (and subfeature) in the collection
134: * has a unique feature id.
135: * <p>
136: * It is considered an error if there is more than root feature with the same id after the
137: * identification of equal features.
138: * <p>
139: * Furthermore, there is always only one feature instance with a certain id, i.e. "equal"
140: * features are represented by the same feature instance.
141: *
142: * @return "merged" feature collection
143: * @throws DatastoreException
144: */
145: FeatureCollection mergeFeatures() throws DatastoreException {
146:
147: assignFIDsAndRepresenters();
148: checkForEqualRootFeatures();
149: replaceDuplicateFeatures();
150: return this .fc;
151: }
152:
153: /**
154: * Assigns a (temporarily) feature id to every anonymous feature (or subfeature) of the given
155: * type in the feature collection.
156: * <p>
157: * Also builds the <code>representerMap</code>, so each feature id is mapped to the feature
158: * instance that is used as the single representer for all features instances with this id.
159: * <p>
160: * It is ensured that for each feature id that is associated with a root feature of the
161: * collection, the root feature is used as the representing feature instance. This is important
162: * to guarantee that the root features in the collection represent themselves and need not to be
163: * replaced in {@link #replaceDuplicateFeatures()}.
164: *
165: * @throws DatastoreException
166: */
167: private void assignFIDsAndRepresenters() 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()
197: + " features of type '" + ft.getName() + "'.");
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
209: .logDebug("Found matching features of type: '"
210: + ft.getName() + "'.");
211: equalFeatures.add(otherFeature);
212: }
213: }
214: assignSameFID(equalFeatures);
215: }
216: }
217:
218: for (Feature feature : features) {
219: String fid = feature.getId();
220: if (this .representerMap.get(fid) == null) {
221: this .representerMap.put(fid, feature);
222: }
223: }
224: }
225:
226: /**
227: * Assigns the same feature id to every feature in the given list of "equal" features.
228: * <p>
229: * If any feature in the list has a feature id assigned to it already, this feature id is used.
230: * If no feature has a feature id, a new feature id (a UUID) is generated.
231: *
232: * @param equalFeatures
233: * @throws DatastoreException
234: */
235: private void assignSameFID(List<Feature> equalFeatures)
236: throws DatastoreException {
237:
238: LOG.logDebug("Found " + equalFeatures.size()
239: + " 'equal' features of type "
240: + equalFeatures.get(0).getFeatureType().getName());
241:
242: String fid = null;
243:
244: // check if any feature has a fid already
245: for (Feature feature : equalFeatures) {
246: String otherFid = feature.getId();
247: if (otherFid != null && !otherFid.equals("")) {
248: if (fid != null && !fid.equals(otherFid)) {
249: String msg = Messages.getMessage(
250: "WFS_IDENTICAL_FEATURES", feature
251: .getFeatureType().getName(), fid,
252: otherFid);
253: throw new DatastoreException(msg);
254: }
255: fid = otherFid;
256: }
257: }
258:
259: if (fid == null) {
260: fid = UUID.randomUUID().toString();
261: this .representerMap.put(fid, equalFeatures.get(0));
262: }
263:
264: // assign fid to every "equal" feature
265: for (Feature feature : equalFeatures) {
266: feature.setId(fid);
267: }
268: }
269:
270: /**
271: * Determines whether two feature instances are "equal" according to the annotated schema
272: * (deegreewfs:IdentityPart declarations).
273: *
274: * @param feature1
275: * @param feature2
276: * @param compareMap
277: * @return true, if the two features are "equal", false otherwise
278: */
279: private boolean compareFeatures(Feature feature1, Feature feature2,
280: Map<Feature, List<Feature>> compareMap) {
281:
282: LOG
283: .logDebug("feature1: " + feature1.getName() + " id="
284: + feature1.getId() + " hashCode="
285: + feature1.hashCode());
286: LOG
287: .logDebug("feature2: " + feature2.getName() + " id="
288: + feature2.getId() + " hashCode="
289: + feature2.hashCode());
290:
291: // same feature instance? -> equal
292: if (feature1 == feature2) {
293: return true;
294: }
295:
296: // same feature id -> equal / different feature id -> not equal
297: String fid1 = feature1.getId();
298: String fid2 = feature2.getId();
299: if (fid1 != null && fid2 != null && !"".equals(fid1)
300: && !"".equals(fid2)) {
301: return fid1.equals(fid2);
302: }
303:
304: // different feature types? -> not equal
305: MappedFeatureType ft = (MappedFeatureType) feature1
306: .getFeatureType();
307: if (feature2.getFeatureType() != ft) {
308: return false;
309: }
310:
311: // feature id is identity part? -> not equal (unique ids for all anonymous features)
312: if (ft.getGMLId().isIdentityPart()) {
313: return false;
314: }
315:
316: // there is already a compareFeatures() call with these features on the stack
317: // -> end recursion
318: List<Feature> features = compareMap.get(feature1);
319: if (features == null) {
320: features = new ArrayList<Feature>();
321: compareMap.put(feature1, features);
322: } else {
323: for (Feature feature : features) {
324: if (feature2 == feature) {
325: return true;
326: }
327: }
328: }
329: features.add(feature2);
330:
331: features = compareMap.get(feature2);
332: if (features == null) {
333: features = new ArrayList<Feature>();
334: compareMap.put(feature2, features);
335: } else {
336: for (Feature feature : features) {
337: if (feature1 == feature) {
338: return true;
339: }
340: }
341: }
342: features.add(feature1);
343:
344: // check every "relevant" property for equality
345: PropertyType[] properties = ft.getProperties();
346: for (int i = 0; i < properties.length; i++) {
347: MappedPropertyType propertyType = (MappedPropertyType) properties[i];
348: if (propertyType.isIdentityPart()) {
349: if (!compareProperties(propertyType, feature1,
350: feature2, compareMap)) {
351: LOG
352: .logDebug("Not equal: values for property '"
353: + propertyType.getName()
354: + " do not match.");
355: return false;
356: }
357: }
358: }
359: return true;
360: }
361:
362: /**
363: * Determines whether two feature instances have the same content in the specified property.
364: *
365: * @param propertyType
366: * @param feature1
367: * @param feature2
368: * @param compareMap
369: * @return true, if the properties are "equal", false otherwise
370: */
371: private boolean compareProperties(MappedPropertyType propertyType,
372: Feature feature1, Feature feature2,
373: Map<Feature, List<Feature>> compareMap) {
374:
375: FeatureProperty[] props1 = feature1.getProperties(propertyType
376: .getName());
377: FeatureProperty[] props2 = feature2.getProperties(propertyType
378: .getName());
379:
380: if (props1 != null && props2 != null) {
381: if (props1.length != props2.length) {
382: return false;
383: }
384: // TODO handle different orders of multi properties
385: for (int j = 0; j < props1.length; j++) {
386: Object value1 = props1[j].getValue();
387: Object value2 = props2[j].getValue();
388:
389: if (value1 == null && value2 == null) {
390: continue;
391: } else if (!(value1 != null && value2 != null)) {
392: return false;
393: }
394:
395: if (value1 instanceof Feature) {
396: // complex valued property (subfeature)
397: if (!(value2 instanceof Feature)) {
398: return false;
399: }
400: Feature subfeature1 = (Feature) value1;
401: Feature subfeature2 = (Feature) value2;
402:
403: if (!compareFeatures(subfeature1, subfeature2,
404: compareMap)) {
405: return false;
406: }
407: } else {
408: if (value1 instanceof Geometry) {
409: String msg = "Check for equal geometry properties is not implemented yet. "
410: + "Do not set 'identityPart' to true in geometry property "
411: + "definitions.";
412: throw new RuntimeException(msg);
413: }
414: // simple valued property
415: if (!value1.equals(value2)) {
416: return false;
417: }
418: }
419: }
420: } else if (!(props1 == null && props2 == null)) {
421: return false;
422: }
423: return true;
424: }
425:
426: /**
427: * Checks that there are no root features in the collection that are "equal".
428: * <p>
429: * After disambiguation these features have the same feature id.
430: *
431: * @throws DatastoreException
432: */
433: private void checkForEqualRootFeatures() throws DatastoreException {
434: Set<String> rootFIDs = new HashSet<String>();
435: for (int i = 0; i < fc.size(); i++) {
436: String fid = fc.getFeature(i).getId();
437: if (rootFIDs.contains(fid)) {
438: String msg = Messages
439: .getMessage("WFS_SAME_ROOT_FEATURE_ID");
440: throw new DatastoreException(msg);
441: }
442: rootFIDs.add(fid);
443: }
444: }
445:
446: /**
447: * Determines the feature type of all features (and subfeatures) in the given feature collection
448: * and builds a lookup map.
449: *
450: * @param fc
451: * @return lookup map that maps each feature instance to it's feature type
452: */
453: private Map<MappedFeatureType, Set<Feature>> buildFeatureTypeMap(
454: FeatureCollection fc) {
455: LOG.logDebug("Building feature map.");
456: Map<MappedFeatureType, Set<Feature>> ftMap = new HashMap<MappedFeatureType, Set<Feature>>();
457: for (int i = 0; i < fc.size(); i++) {
458: addToFeatureTypeMap(fc.getFeature(i), ftMap);
459: }
460: return ftMap;
461: }
462:
463: /**
464: * Recursively adds the given feature (and it's subfeatures) to the given map. To cope with
465: * cyclic features, the recursion ends if the feature instance is already present in the map.
466: *
467: * @param feature
468: * feature instance to add
469: * @param ftMap
470: */
471: private void addToFeatureTypeMap(Feature feature,
472: Map<MappedFeatureType, Set<Feature>> ftMap) {
473:
474: MappedFeatureType ft = (MappedFeatureType) feature
475: .getFeatureType();
476: Set<Feature> features = ftMap.get(ft);
477: if (features == null) {
478: features = new HashSet<Feature>();
479: ftMap.put(ft, features);
480: } else {
481: if (features.contains(feature)) {
482: return;
483: }
484: }
485: features.add(feature);
486:
487: // recurse into subfeatures
488: FeatureProperty[] properties = feature.getProperties();
489: for (int i = 0; i < properties.length; i++) {
490: Object propertyValue = properties[i].getValue();
491: if (propertyValue instanceof Feature) {
492: Feature subFeature = (Feature) propertyValue;
493: addToFeatureTypeMap(subFeature, ftMap);
494: }
495: }
496: }
497:
498: /**
499: * Ensures that all features with the same feature id refer to the same feature instance.
500: * <p>
501: * Xlinked content is used for every subfeature that has been encountered already (or is a root
502: * feature of the collection).
503: * <p>
504: * Root features are never replaced, because {@link #assignFIDsAndRepresenters()} ensures that
505: * root features always represent themselves.
506: */
507: private void replaceDuplicateFeatures() {
508:
509: Set<String> xlinkFIDs = new HashSet<String>();
510:
511: // ensure that root features are always referred to by xlink properties
512: for (int i = 0; i < this .fc.size(); i++) {
513: Feature feature = this .fc.getFeature(i);
514: xlinkFIDs.add(feature.getId());
515: }
516:
517: for (int i = 0; i < this .fc.size(); i++) {
518: Feature feature = this .fc.getFeature(i);
519: replaceDuplicateFeatures(feature, xlinkFIDs);
520: }
521: }
522:
523: /**
524: * Replaces all subfeatures of the given feature instance by their "representers".
525: * <p>
526: * Xlinked content is used for every subfeature that has been encountered already (or that is a
527: * root feature of the collection).
528: *
529: * @param feature
530: * @param xlinkFIDs
531: */
532: private void replaceDuplicateFeatures(Feature feature,
533: Set<String> xlinkFIDs) {
534:
535: LOG.logDebug("Replacing in feature: '" + feature.getName()
536: + "', " + feature.getId());
537: xlinkFIDs.add(feature.getId());
538:
539: FeatureProperty[] properties = feature.getProperties();
540: for (int i = 0; i < properties.length; i++) {
541: Object value = properties[i].getValue();
542: if (value != null && value instanceof Feature) {
543:
544: Feature subfeature = (Feature) value;
545: String fid = subfeature.getId();
546: subfeature = this .representerMap.get(fid);
547:
548: FeatureProperty oldProperty = properties[i];
549: FeatureProperty newProperty = null;
550:
551: if (!xlinkFIDs.contains(fid)) {
552: // first occurence in feature collection
553: LOG.logDebug("Not-XLink feature property: " + fid);
554: newProperty = FeatureFactory.createFeatureProperty(
555: oldProperty.getName(), subfeature);
556: replaceDuplicateFeatures(subfeature, xlinkFIDs);
557: } else {
558: // successive occurence in feature collection (use XLink)
559: LOG.logDebug("XLink feature property: " + fid);
560: newProperty = new XLinkedFeatureProperty(
561: oldProperty.getName(), fid);
562: newProperty.setValue(subfeature);
563: }
564: feature.replaceProperty(oldProperty, newProperty);
565: }
566: }
567: }
568: }
|