001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2002-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.feature;
017:
018: import java.rmi.server.UID;
019: import java.util.List;
020: import java.util.regex.Pattern;
021:
022: import org.geotools.geometry.jts.ReferencedEnvelope;
023: import org.opengis.util.Cloneable;
024:
025: import com.vividsolutions.jts.geom.Envelope;
026: import com.vividsolutions.jts.geom.Geometry;
027:
028: /**
029: * Provides a more efficient feature representation for the flat and complex
030: * features. This implementation actually not only enforces feature type
031: * synchronization, it also enforces the use of its accessor methods to change
032: * the state of internal object representations. In this case, the
033: * implementation is trivial, since all allowed attribute objects (from the
034: * feature type) are immutable.
035: *
036: * @author Chris Holmes, TOPP <br>
037: * @author Rob Hranac, TOPP
038: * @author Ian Schneider ARS-USDA
039: *
040: * @task TODO: look at synchronization (or locks as IanS thinks)
041: * @task REVISIT: Right now we always validate, which means whenever a Feature
042: * is created or a new value set then an operation must be performed.
043: * One thing we should consider is to allow a Feature to turn off its
044: * its validation - which would likely improve performance with large
045: * datasets. If you are reading from a database, with a FeatureType you
046: * got from the database, it is probably a reasonable assumption that
047: * the Features contained in it will properly validate. I am not sure
048: * if this should with a switch in DefaultFeature, or perhaps an
049: * interface that says if it is validating or not, or maybe even an
050: * option in Feature. But it would be a nice option to have - if
051: * datastore implementors could at least create their features without
052: * validating (though probably should return Features that will check
053: * for validity if someone else tries to change them).
054: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/feature/DefaultFeature.java $
055: */
056: public class DefaultFeature implements SimpleFeature, Cloneable {
057: /** The unique id of this feature */
058: protected String featureId;
059:
060: /** Flat feature type schema for this feature. */
061: private final DefaultFeatureType schema;
062:
063: /** Attributes for the feature. */
064: private Object[] attributes;
065:
066: /** The bounds of this feature. */
067: private Envelope bounds;
068:
069: /** The collection that this Feature is a member of */
070: private FeatureCollection parent;
071:
072: // private static final Pattern NON_WORD_PATTERN = Pattern.compile(":");
073:
074: /**
075: * Creates a new instance of flat feature, which must take a flat feature
076: * type schema and all attributes as arguments.
077: *
078: * @param schema Feature type schema for this flat feature.
079: * @param attributes Initial attributes for this feature.
080: * @param featureID The unique ID for this feature.
081: *
082: * @throws IllegalAttributeException Attribtues do not conform to feature
083: * type schema.
084: * @throws NullPointerException if schema is null.
085: */
086: protected DefaultFeature(DefaultFeatureType schema,
087: Object[] attributes, String featureID)
088: throws IllegalAttributeException, NullPointerException {
089: if (schema == null) {
090: throw new NullPointerException("schema");
091: }
092:
093: this .schema = schema;
094: this .featureId = (featureID == null) ? defaultID() : featureID;
095: this .attributes = new Object[schema.getAttributeCount()];
096:
097: setAttributes(attributes);
098: }
099:
100: /**
101: * Creates a new instance of flat feature, which must take a flat feature
102: * type schema and all attributes as arguments.
103: *
104: * @param schema Feature type schema for this flat feature.
105: * @param attributes Initial attributes for this feature.
106: *
107: * @throws IllegalAttributeException Attribtues do not conform to feature
108: * type schema.
109: *
110: * @task REVISIT: should we allow this? Force users to explicitly set
111: * featureID to null?
112: */
113: protected DefaultFeature(DefaultFeatureType schema,
114: Object[] attributes) throws IllegalAttributeException {
115: this (schema, attributes, null);
116: }
117:
118: /**
119: * Creates an ID from a hashcode.
120: *
121: * @return an id for the feature.
122: */
123: String defaultID() {
124: // According to GML and XML schema standards, FID is a XML ID
125: // (http://www.w3.org/TR/xmlschema-2/#ID), whose acceptable values are those that match an
126: // NCNAME production (http://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName):
127: // NCName ::= (Letter | '_') (NCNameChar)* /* An XML Name, minus the ":" */
128: // NCNameChar ::= Letter | Digit | '.' | '-' | '_' | CombiningChar | Extender
129: // We have to fix the generated UID replacing all non word chars with an _ (it seems
130: // they area all ":")
131: // return "fid-" + NON_WORD_PATTERN.matcher(new UID().toString()).replaceAll("_");
132: // optimization, since the UID toString uses only ":" and converts long and integers
133: // to strings for the rest, so the only non word character is really ":"
134: return "fid-" + new UID().toString().replace(':', '_');
135: }
136:
137: /**
138: * Gets a reference to the feature type schema for this feature.
139: *
140: * @return A copy of this feature's metadata in the form of a feature type
141: * schema.
142: */
143: public FeatureType getFeatureType() {
144: return schema;
145: }
146:
147: /**
148: * Gets the unique indentification string of this Feature.
149: *
150: * @return The unique id.
151: */
152: public String getID() {
153: return featureId;
154: }
155:
156: /**
157: * Copy all the attributes of this Feature into the given array. If the
158: * argument array is null, a new one will be created. Gets all attributes
159: * from this feature, returned as a complex object array. This array
160: * comes with no metadata, so to interpret this collection the caller
161: * class should ask for the schema as well.
162: *
163: * @param array The array to copy the attributes into.
164: *
165: * @return The array passed in, or a new one if null.
166: */
167: public Object[] getAttributes(Object[] array) {
168: Object[] retArray;
169:
170: if (array == null) {
171: retArray = new Object[attributes.length];
172: } else {
173: retArray = array;
174: }
175:
176: System.arraycopy(attributes, 0, retArray, 0, attributes.length);
177:
178: return retArray;
179: }
180:
181: /**
182: * Gets an attribute for this feature at the location specified by xPath.
183: *
184: * @param xPath XPath representation of attribute location.
185: *
186: * @return Attribute.
187: */
188: public Object getAttribute(String xPath) {
189: int idx = schema.find(xPath);
190:
191: if (idx == -1) {
192: return null;
193: }
194:
195: return attributes[idx];
196: }
197:
198: /**
199: * Gets an attribute by the given zero-based index.
200: *
201: * @param index the position of the attribute to retrieve.
202: *
203: * @return The attribute at the given index.
204: */
205: public Object getAttribute(int index) {
206: return attributes[index];
207: }
208:
209: /**
210: * Sets the attribute at position to val.
211: *
212: * @param position the index of the attribute to set.
213: * @param val the new value to give the attribute at position.
214: *
215: * @throws IllegalAttributeException if the passed in val does not validate
216: * against the AttributeType at that position.
217: */
218: public void setAttribute(int position, Object val)
219: throws IllegalAttributeException {
220: AttributeType type = schema.getAttributeType(position);
221:
222: try {
223: if ((val == null) && !type.isNillable())
224: val = type.createDefaultValue();
225: Object parsed = type.parse(val);
226: type.validate(parsed);
227: setAttributeValue(position, parsed);
228: } catch (IllegalArgumentException iae) {
229: throw new IllegalAttributeException(type, val, iae);
230: }
231: }
232:
233: /**
234: * Sets the attribute value at a given position, performing no parsing or
235: * validation. This is so subclasses can have access to setting the array,
236: * without opening it up completely.
237: *
238: * @param position the index of the attribute to set.
239: * @param val the new value to give the attribute at position.
240: */
241: protected void setAttributeValue(int position, Object val) {
242: attributes[position] = val;
243: }
244:
245: /**
246: * Sets all attributes for this feature, passed as an array. All
247: * attributes are checked for validity before adding.
248: *
249: * @param attributes All feature attributes.
250: *
251: * @throws IllegalAttributeException Passed attributes do not match feature
252: * type.
253: */
254: public void setAttributes(Object[] attributes)
255: throws IllegalAttributeException {
256: // the passed in attributes were null, lets make that a null array
257: Object[] newAtts = attributes;
258:
259: if (attributes == null) {
260: newAtts = new Object[this .attributes.length];
261: }
262:
263: if (newAtts.length != this .attributes.length) {
264: throw new IllegalAttributeException(
265: "Wrong number of attributes expected "
266: + schema.getAttributeCount() + " got "
267: + newAtts.length);
268: }
269:
270: for (int i = 0, ii = newAtts.length; i < ii; i++) {
271: setAttribute(i, newAtts[i]);
272: }
273: }
274:
275: /**
276: * Sets a single attribute for this feature, passed as a complex object. If
277: * the attribute does not exist or the object does not conform to the
278: * internal feature type, an exception is thrown.
279: *
280: * @param xPath XPath representation of attribute location.
281: * @param attribute Feature attribute to set.
282: *
283: * @throws IllegalAttributeException Passed attribute does not match
284: * feature type
285: */
286: public void setAttribute(String xPath, Object attribute)
287: throws IllegalAttributeException {
288: int idx = schema.find(xPath);
289:
290: if (idx < 0) {
291: throw new IllegalAttributeException("No attribute named "
292: + xPath);
293: }
294:
295: setAttribute(idx, attribute);
296: }
297:
298: /**
299: * Gets the geometry for this feature.
300: *
301: * @return Geometry for this feature.
302: */
303: public Geometry getDefaultGeometry() {
304: int idx = schema.defaultGeomIdx;
305:
306: if (idx == -1) {
307: return null;
308: }
309:
310: return (Geometry) attributes[idx];
311: }
312:
313: /**
314: * Modifies the geometry.
315: *
316: * @param geometry All feature attributes.
317: *
318: * @throws IllegalAttributeException if the feature does not have a
319: * geometry.
320: */
321: public void setDefaultGeometry(Geometry geometry)
322: throws IllegalAttributeException {
323: int idx = schema.defaultGeomIdx;
324:
325: if (idx < 0) {
326: throw new IllegalAttributeException(
327: "Feature does not have geometry");
328: }
329:
330: attributes[idx] = geometry;
331: bounds = null;
332: }
333:
334: /**
335: * Get the number of attributes this feature has. This is simply a
336: * convenience method for calling
337: * getFeatureType().getNumberOfAttributes();
338: *
339: * @return The total number of attributes this Feature contains.
340: */
341: public int getNumberOfAttributes() {
342: return attributes.length;
343: }
344:
345: /**
346: * Get the total bounds of this feature which is calculated by doing a
347: * union of the bounds of each geometry this feature is associated with.
348: *
349: * @return An Envelope containing the total bounds of this Feature.
350: *
351: * @task REVISIT: what to return if there are no geometries in the feature?
352: * For now we'll return a null envelope, make this part of
353: * interface? (IanS - by OGC standards, all Feature must have geom)
354: */
355: public ReferencedEnvelope getBounds() {
356: if (bounds == null) {
357: bounds = new Envelope();
358:
359: for (int i = 0, n = schema.getAttributeCount(); i < n; i++) {
360: if (schema.getAttributeType(i) instanceof GeometryAttributeType) {
361: Geometry g = (Geometry) attributes[i];
362:
363: // IanS - check for null geometry!
364: if (g == null) {
365: continue;
366: }
367:
368: Envelope e = g.getEnvelopeInternal();
369:
370: // IanS
371: // as of JTS 1.3, expandToInclude does not check to see if
372: // Envelope is "null", and simply adds the flagged values.
373: // This ensures that this behavior does not occur.
374: if (!e.isNull()) {
375: bounds.expandToInclude(e);
376: }
377: }
378: }
379: }
380:
381: // lets be defensive
382: if (schema.getPrimaryGeometry() != null) {
383: return new ReferencedEnvelope(bounds, schema
384: .getPrimaryGeometry().getCoordinateSystem());
385: }
386: return new ReferencedEnvelope(bounds, null);
387: }
388:
389: /**
390: * Creates an exact copy of this feature.
391: *
392: * @return A default feature.
393: *
394: * @throws RuntimeException DOCUMENT ME!
395: */
396: public Object clone() {
397: try {
398: DefaultFeature clone = (DefaultFeature) super .clone();
399:
400: for (int i = 0; i < attributes.length; i++) {
401: try {
402: clone.setAttribute(i, attributes[i]);
403: } catch (IllegalAttributeException e1) {
404: throw new RuntimeException(
405: "The impossible has happened", e1);
406: }
407: }
408:
409: return clone;
410: } catch (CloneNotSupportedException e) {
411: throw new RuntimeException("The impossible has happened", e);
412: }
413: }
414:
415: /**
416: * Returns a string representation of this feature.
417: *
418: * @return A representation of this feature as a string.
419: */
420: public String toString() {
421: String retString = "Feature[ id=" + getID() + " , ";
422: FeatureType featType = getFeatureType();
423:
424: for (int i = 0, n = attributes.length; i < n; i++) {
425: retString += (featType.getAttributeType(i).getName() + "=");
426: retString += attributes[i];
427:
428: if ((i + 1) < n) {
429: retString += " , ";
430: }
431: }
432:
433: return retString += " ]";
434: }
435:
436: /**
437: * returns a unique code for this feature
438: *
439: * @return A unique int
440: */
441: public int hashCode() {
442: return featureId.hashCode() * schema.hashCode();
443: }
444:
445: /**
446: * override of equals. Returns if the passed in object is equal to this.
447: *
448: * @param obj the Object to test for equality.
449: *
450: * @return <code>true</code> if the object is equal, <code>false</code>
451: * otherwise.
452: */
453: public boolean equals(Object obj) {
454: if (obj == null) {
455: return false;
456: }
457:
458: if (obj == this ) {
459: return true;
460: }
461:
462: if (!(obj instanceof Feature)) {
463: return false;
464: }
465:
466: Feature feat = (Feature) obj;
467:
468: if (!feat.getFeatureType().equals(schema)) {
469: return false;
470: }
471:
472: // this check shouldn't exist, by contract,
473: //all features should have an ID.
474: if (featureId == null) {
475: if (feat.getID() != null) {
476: return false;
477: }
478: }
479:
480: if (!featureId.equals(feat.getID())) {
481: return false;
482: }
483:
484: for (int i = 0, ii = attributes.length; i < ii; i++) {
485: Object otherAtt = feat.getAttribute(i);
486:
487: if (attributes[i] == null) {
488: if (otherAtt != null) {
489: return false;
490: }
491: } else {
492: if (!attributes[i].equals(otherAtt)) {
493: if (attributes[i] instanceof Geometry
494: && otherAtt instanceof Geometry) {
495: // we need to special case Geometry
496: // as JTS is broken
497: // Geometry.equals( Object ) and Geometry.equals( Geometry )
498: // are different
499: // (We should fold this knowledge into AttributeType...)
500: //
501: if (!((Geometry) attributes[i])
502: .equals((Geometry) otherAtt)) {
503: return false;
504: }
505: } else {
506: return false;
507: }
508: }
509: }
510: }
511:
512: return true;
513: }
514:
515: /**
516: * Gets the feature collection this feature is stored in.
517: *
518: * @return the collection that is the parent of this feature.
519: */
520: public FeatureCollection getParent() {
521: return parent;
522: }
523:
524: /**
525: * Sets the parent collection this feature is stored in, if it is not
526: * already set. If it is set then this method does nothing.
527: *
528: * @param collection the collection to be set as parent.
529: */
530: public void setParent(FeatureCollection collection) {
531: if (parent == null) {
532: parent = collection;
533: }
534: }
535:
536: public Feature toComplex() {
537: try {
538: return new ComplexWrapper(this );
539: } catch (IllegalAttributeException iae) {
540: throw new RuntimeException("the impossible has happened: ",
541: iae);
542: }
543: }
544:
545: /**
546: * This class will wrap a DefaultFeature (which is a {@link SimpleFeature}
547: * into a Feature with multiplicity - which is to say it will always
548: * return a List of its attributes when they are requested. These will
549: * always be singleton Lists, since the min/max of attributes in a
550: * DefaultFeature is 1. But this is important so that clients can deal
551: * with all Features in the same way - always expecting Lists.
552: *
553: * @author Chris Holmes, Fulbright
554: */
555: static final class ComplexWrapper extends DefaultFeature {
556: /**
557: * Private constructor to wrap the attributes in list. Could consider
558: * making this public, but for now it seems better to keep it private
559: * since we do no check to make sure tha attribute array isn't already
560: * complex - and thus if it was we would wrap it in Lists again.
561: *
562: * @param fType DOCUMENT ME!
563: * @param atts DOCUMENT ME!
564: * @param fid DOCUMENT ME!
565: *
566: * @throws IllegalAttributeException DOCUMENT ME!
567: */
568: private ComplexWrapper(DefaultFeatureType fType, Object[] atts,
569: String fid) throws IllegalAttributeException {
570: super (fType, wrapInList(atts, fType.getAttributeCount()),
571: fid);
572: }
573:
574: public ComplexWrapper(DefaultFeatureType fType, Object[] atts)
575: throws IllegalAttributeException {
576: this (fType, atts, null);
577: }
578:
579: //This could be problematic, not sure if all SimpleFeatures will have
580: //DefaultFeatureTypes.
581: public ComplexWrapper(SimpleFeature feature)
582: throws IllegalAttributeException {
583: this ((DefaultFeatureType) feature.getFeatureType(), feature
584: .getAttributes(null), feature.getID());
585: }
586:
587: /**
588: * Sets the attribute Object at the given index. As this is a complex
589: * feature - one with multiplicity - then all attributes passed in
590: * must be Lists. Since this is just a wrapped SimpleFeature, the
591: * List passed in will only ever be a singleton, but we must abide by
592: * the contract of a ComplexFeature.
593: *
594: * @param index The attribute to set.
595: * @param value A Singleton List of the attribute to set.
596: *
597: * @throws IllegalAttributeException If the value is not a singleton
598: * List.
599: *
600: * @task REVISIT: Make List explicit in Java 1.5
601: * @task REVISIT: We could consider accepting none list objects, and
602: * justify it as part of the parse method - that is to say that
603: * most calls to setAttribute will turn the Object into the
604: * proper form, so why not here? For a true Complex Feature this
605: * has implications of letting people blow away their multiple
606: * attributes, but for here there are no such dangers. -ch
607: */
608: public void setAttribute(int index, Object value)
609: throws IllegalAttributeException {
610: checkList(value);
611:
612: List valList = (List) value;
613: int listSize = valList.size();
614:
615: if (listSize == 0) {
616: super .setAttribute(index, wrapInList(null));
617: } else {
618: AttributeType type = super .getFeatureType()
619: .getAttributeType(index);
620: Object val = valList.get(0);
621:
622: try {
623: Object parsed = type.parse(val);
624: type.validate(parsed);
625: setAttributeValue(index, wrapInList(parsed));
626: } catch (IllegalArgumentException iae) {
627: throw new IllegalAttributeException(type, val, iae);
628: }
629: }
630: }
631:
632: private void checkList(Object value)
633: throws IllegalAttributeException {
634: if (value instanceof List) {
635: List valList = (List) value;
636: int listSize = valList.size();
637:
638: if (listSize > 1) {
639: String errMsg = "The attribute: " + valList
640: + " has more " + "attributes (" + listSize
641: + ") than is allowed by an "
642: + " attributeType in a Simple Feature (1)";
643: throw new IllegalAttributeException(errMsg);
644: }
645: } else {
646: String errMsg = "All objects set in a ComplexFeature must be "
647: + "Lists, to account for multiplicity";
648: throw new IllegalAttributeException(errMsg);
649: }
650: }
651:
652: /**
653: * Sets the attribute at the given xPath. Note that right now this
654: * just does the name, and will fail on anything other than the name.
655: *
656: * @param xPath The name of the attribute to Set.
657: * @param attribute The value to set - must be a List, for this Complex
658: * Feature.
659: *
660: * @throws IllegalAttributeException DOCUMENT ME!
661: *
662: * @task TODO: Revisit xPath stuff - get it working or do external
663: * implementation.
664: */
665: public void setAttribute(String xPath, Object attribute)
666: throws IllegalAttributeException {
667: int idx = super .getFeatureType().find(xPath);
668:
669: if (idx < 0) {
670: throw new IllegalAttributeException(
671: "No attribute named " + xPath);
672: }
673:
674: setAttribute(idx, attribute);
675: }
676:
677: protected static List wrapInList(Object attribute) {
678: return java.util.Collections.singletonList(attribute);
679: }
680:
681: protected static Object[] wrapInList(Object[] attributes,
682: int defaultSize) {
683: Object[] retArray = attributes;
684:
685: if (attributes == null) {
686: retArray = new Object[defaultSize];
687: } else {
688: retArray = attributes;
689: }
690:
691: for (int i = 0; i < attributes.length; i++) {
692: retArray[i] = wrapInList(attributes[i]);
693: }
694:
695: return retArray;
696: }
697: }
698: }
|