/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2008, Open Source Geospatial Foundation (OSGeo) * * 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; * version 2.1 of the License. * * 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. */ package org.geotools.feature; import java.rmi.server.UID; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.feature.simple.SimpleFeatureImpl; import org.geotools.feature.type.DefaultFeatureTypeBuilder; import org.geotools.feature.type.GeometricAttributeType; import org.geotools.geometry.jts.ReferencedEnvelope; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.util.Cloneable; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; /** * Provides a more efficient feature representation for the flat and complex * features. This implementation actually not only enforces feature type * synchronization, it also enforces the use of its accessor methods to change * the state of internal object representations. In this case, the * implementation is trivial, since all allowed attribute objects (from the * feature type) are immutable. * * @author Chris Holmes, TOPP <br> * @author Rob Hranac, TOPP * @author Ian Schneider ARS-USDA * * @task TODO: look at synchronization (or locks as IanS thinks) * @task REVISIT: Right now we always validate, which means whenever a Feature * is created or a new value set then an operation must be performed. * One thing we should consider is to allow a Feature to turn off its * its validation - which would likely improve performance with large * datasets. If you are reading from a database, with a FeatureType you * got from the database, it is probably a reasonable assumption that * the Features contained in it will properly validate. I am not sure * if this should with a switch in DefaultFeature, or perhaps an * interface that says if it is validating or not, or maybe even an * option in Feature. But it would be a nice option to have - if * datastore implementors could at least create their features without * validating (though probably should return Features that will check * for validity if someone else tries to change them). * @source $URL$ */ public class DefaultFeature extends SimpleFeatureImpl implements SimpleFeature, Cloneable { // /** The unique id of this feature */ // protected String featureId; // // /** Flat feature type schema for this feature. */ // private final DefaultFeatureType schema; // // /** Attributes for the feature. */ // private Object[] attributes; // // /** The bounds of this feature. */ // private Envelope bounds; /** The collection that this Feature is a member of */ private FeatureCollection<SimpleFeatureType, org.opengis.feature.simple.SimpleFeature> parent; // private static final Pattern NON_WORD_PATTERN = Pattern.compile(":"); /** * Creates a new instance of flat feature, which must take a flat feature * type schema and all attributes as arguments. * * @param schema Feature type schema for this flat feature. * @param attributes Initial attributes for this feature. * @param featureID The unique ID for this feature. * * @throws IllegalAttributeException Attribtues do not conform to feature * type schema. * @throws NullPointerException if schema is null. */ protected DefaultFeature(DefaultFeatureType schema, Object[] attributes, String featureID) throws IllegalAttributeException, NullPointerException { //super( Arrays.asList(attributes), schema, featureID ); super(wrapValues(attributes,schema), schema, SimpleFeatureBuilder.createDefaultFeatureIdentifier(featureID) ); if (schema == null) { throw new NullPointerException("schema"); } // this.schema = schema; // this.featureId = (featureID == null) ? defaultID() : featureID; // this.attributes = new Object[schema.getAttributeCount()]; // // setAttributes(attributes); } /** * wraps an array of raw values in attributes. * */ static List wrapValues( Object[] values, DefaultFeatureType schema ) { ArrayList atts = new ArrayList(values.length); for ( int i = 0; i < values.length; i++ ) { atts.add( new AttributeImpl( values[i], schema.getDescriptor(i),null) { public void set(Object newValue) throws IllegalArgumentException, IllegalStateException { newValue = parse(newValue); value = newValue; } } ); } return atts; } /** * Creates a new instance of flat feature, which must take a flat feature * type schema and all attributes as arguments. * * @param schema Feature type schema for this flat feature. * @param attributes Initial attributes for this feature. * * @throws IllegalAttributeException Attribtues do not conform to feature * type schema. * * @task REVISIT: should we allow this? Force users to explicitly set * featureID to null? */ protected DefaultFeature(DefaultFeatureType schema, Object[] attributes) throws IllegalAttributeException { this(schema, attributes, null); } public DefaultFeature(List/*<Attribute>*/ attributes,DefaultFeatureType schema, String id ) { super( attributes, schema, SimpleFeatureBuilder.createDefaultFeatureIdentifier(id) ); } /** * Creates an ID from a hashcode. * * @return an id for the feature. */ String defaultID() { // According to GML and XML schema standards, FID is a XML ID // (http://www.w3.org/TR/xmlschema-2/#ID), whose acceptable values are those that match an // NCNAME production (http://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-NCName): // NCName ::= (Letter | '_') (NCNameChar)* /* An XML Name, minus the ":" */ // NCNameChar ::= Letter | Digit | '.' | '-' | '_' | CombiningChar | Extender // We have to fix the generated UID replacing all non word chars with an _ (it seems // they area all ":") // return "fid-" + NON_WORD_PATTERN.matcher(new UID().toString()).replaceAll("_"); // optimization, since the UID toString uses only ":" and converts long and integers // to strings for the rest, so the only non word character is really ":" return "fid-" + new UID().toString().replace(':', '_'); } /** * Gets a reference to the feature type schema for this feature. * * @return A copy of this feature's metadata in the form of a feature type * schema. */ public FeatureType getFeatureType() { return (FeatureType) getType(); } /** * Gets the unique indentification string of this Feature. * * @return The unique id. */ // public String getID() { // return featureId; // } /** * Copy all the attributes of this Feature into the given array. If the * argument array is null, a new one will be created. Gets all attributes * from this feature, returned as a complex object array. This array * comes with no metadata, so to interpret this collection the caller * class should ask for the schema as well. * * @param array The array to copy the attributes into. * * @return The array passed in, or a new one if null. */ public Object[] getAttributes(Object[] array) { Object[] retArray; List values = getAttributes(); if (array == null) { //retArray = new Object[attributes.length]; retArray = new Object[values.size()]; } else { retArray = array; } //System.arraycopy(attributes, 0, retArray, 0, attributes.length); //return retArray; return values.toArray( retArray ); } /** * Gets an attribute for this feature at the location specified by xPath. * * @param xPath XPath representation of attribute location. * * @return Attribute. */ // public Object getAttribute(String xPath) { // // int idx = schema.find(xPath); // // if (idx == -1) { // return null; // } // // return attributes[idx]; // } /** * Gets an attribute by the given zero-based index. * * @param index the position of the attribute to retrieve. * * @return The attribute at the given index. */ // public Object getAttribute(int index) { // return attributes[index]; // } /** * Sets the attribute at position to val. * * @param position the index of the attribute to set. * @param val the new value to give the attribute at position. * * @throws IllegalAttributeException if the passed in val does not validate * against the AttributeType at that position. */ public void setAttribute(int position, Object val) { try { super.setAttribute(position, val); } catch( IndexOutOfBoundsException ioobe ) { throw ioobe; } catch( Exception e ) { throw (IllegalAttributeException) new IllegalAttributeException(e.getLocalizedMessage()).initCause(e); } // AttributeType type = schema.getAttributeType(position); // // try { // if ((val == null) && !type.isNillable()) val = type.createDefaultValue(); // Object parsed = type.parse(val); // type.validate(parsed); // setAttributeValue(position, parsed); // } catch (IllegalArgumentException iae) { // throw new IllegalAttributeException(type, val, iae); // } } /** * Sets the attribute value at a given position, performing no parsing or * validation. This is so subclasses can have access to setting the array, * without opening it up completely. * * @param position the index of the attribute to set. * @param val the new value to give the attribute at position. */ // protected void setAttributeValue(int position, Object val) { // attributes[position] = val; // } /** * Sets all attributes for this feature, passed as an array. All * attributes are checked for validity before adding. * * @param attributes All feature attributes. * * @throws IllegalAttributeException Passed attributes do not match feature * type. */ public void setAttributes(Object[] attributes) throws IllegalAttributeException { try { super.setAttributes(attributes); } catch( Exception e ) { throw (IllegalAttributeException) new IllegalAttributeException(e.getLocalizedMessage()).initCause(e); } // // // // the passed in attributes were null, lets make that a null array // Object[] newAtts = attributes; // // if (attributes == null) { // newAtts = new Object[this.attributes.length]; // } // // if (newAtts.length != this.attributes.length) { // throw new IllegalAttributeException( // "Wrong number of attributes expected " // + schema.getAttributeCount() + " got " + newAtts.length); // } // // for (int i = 0, ii = newAtts.length; i < ii; i++) { // setAttribute(i, newAtts[i]); // } } /** * Sets a single attribute for this feature, passed as a complex object. If * the attribute does not exist or the object does not conform to the * internal feature type, an exception is thrown. * * @param xPath XPath representation of attribute location. * @param attribute Feature attribute to set. * * @throws IllegalAttributeException Passed attribute does not match * feature type */ public void setAttribute(String xPath, Object attribute) throws IllegalAttributeException { try { super.setAttribute(xPath, attribute); } catch( Exception e ) { throw (IllegalAttributeException) new IllegalAttributeException(e.getLocalizedMessage()).initCause(e); } // int idx = schema.find(xPath); // // if (idx < 0) { // throw new IllegalAttributeException("No attribute named " + xPath); // } // // setAttribute(idx, attribute); } /** * Gets the geometry for this feature. * * @return Geometry for this feature. * @deprecated use {@link #getDefaultGeometry()}. */ public Geometry getDefaultGeometry() { return (Geometry) super.getDefaultGeometry(); // int idx = schema.defaultGeomIdx; // // if (idx == -1) { // return null; // } // // return (Geometry) attributes[idx]; } /** * Modifies the geometry. * * @param geometry All feature attributes. * * @throws IllegalAttributeException if the feature does not have a * geometry. * @deprecated use {@link #setDefaultGeometry(Geometry)}. */ public void setDefaultGeometry(Geometry geometry) throws IllegalAttributeException { try { super.setDefaultGeometry(geometry); } catch( Exception e ) { throw (IllegalAttributeException) new IllegalAttributeException(e.getLocalizedMessage()).initCause(e); } // int idx = schema.defaultGeomIdx; // // if (idx < 0) { // throw new IllegalAttributeException( // "Feature does not have geometry"); // } // // attributes[idx] = geometry; // bounds = null; } /** * Get the number of attributes this feature has. This is simply a * convenience method for calling * getFeatureType().getNumberOfAttributes(); * * @return The total number of attributes this Feature contains. */ public int getNumberOfAttributes() { return getAttributeCount(); //return attributes.length; } /** * Get the total bounds of this feature which is calculated by doing a * union of the bounds of each geometry this feature is associated with. * * @return An Envelope containing the total bounds of this Feature. * * @task REVISIT: what to return if there are no geometries in the feature? * For now we'll return a null envelope, make this part of * interface? (IanS - by OGC standards, all Feature must have geom) */ public ReferencedEnvelope getBounds() { return (ReferencedEnvelope) super.getBounds(); // if (bounds == null) { // bounds = new Envelope(); // // for (int i = 0, n = schema.getAttributeCount(); i < n; i++) { // if (schema.getAttributeType(i) instanceof GeometryAttributeType ) { // Geometry g = (Geometry) attributes[i]; // // // IanS - check for null geometry! // if (g == null) { // continue; // } // // Envelope e = g.getEnvelopeInternal(); // // // IanS // // as of JTS 1.3, expandToInclude does not check to see if // // Envelope is "null", and simply adds the flagged values. // // This ensures that this behavior does not occur. // if (!e.isNull()) { // bounds.expandToInclude(e); // } // } // } // } // // // lets be defensive // if (schema.getPrimaryGeometry() != null) { // return new ReferencedEnvelope(bounds,schema.getPrimaryGeometry().getCoordinateSystem()); // } // return new ReferencedEnvelope(bounds,null); } /** * Creates an exact copy of this feature. * * @return A default feature. * * @throws RuntimeException DOCUMENT ME! */ public Object clone() { try { DefaultFeature clone = (DefaultFeature) super.clone(); clone.setValue(getValue()); // for (int i = 0; i < values.size(); i++) { // try { // clone.setAttribute(i // } catch (IllegalAttributeException e1) { // throw new RuntimeException("The impossible has happened", e1); // } // } return clone; } catch (CloneNotSupportedException e) { throw new RuntimeException("The impossible has happened", e); } } /** * Returns a string representation of this feature. * * @return A representation of this feature as a string. */ public String toString() { String retString = "Feature[ id=" + getID() + " , "; FeatureType featType = getFeatureType(); List attributes = getAttributes(); for (int i = 0, n = attributes.size(); i < n; i++) { retString += (featType.getAttributeType(i).getLocalName() + "="); retString += attributes.get(i); if ((i + 1) < n) { retString += " , "; } } return retString += " ]"; } /** * returns a unique code for this feature * * @return A unique int */ // public int hashCode() { // return featureId.hashCode() * schema.hashCode(); // } /** * override of equals. Returns if the passed in object is equal to this. * * @param obj the Object to test for equality. * * @return <code>true</code> if the object is equal, <code>false</code> * otherwise. */ public boolean equals(Object obj) { if (obj == null) { return false; } if (obj == this) { return true; } if (!(obj instanceof Feature)) { return false; } return super.equals( obj ); // Feature feat = (Feature) obj; // // if (!feat.getFeatureType().equals(schema)) { // return false; // } // // // this check shouldn't exist, by contract, // //all features should have an ID. // if (featureId == null) { // if (feat.getID() != null) { // return false; // } // } // // if (!featureId.equals(feat.getID())) { // return false; // } // // for (int i = 0, ii = attributes.length; i < ii; i++) { // Object otherAtt = feat.getAttribute(i); // // if (attributes[i] == null) { // if (otherAtt != null) { // return false; // } // } else { // if (!attributes[i].equals(otherAtt)) { // if (attributes[i] instanceof Geometry // && otherAtt instanceof Geometry) { // // we need to special case Geometry // // as JTS is broken // // Geometry.equals( Object ) and Geometry.equals( Geometry ) // // are different // // (We should fold this knowledge into AttributeType...) // // // if (!((Geometry) attributes[i]).equals( // (Geometry) otherAtt)) { // return false; // } // } else { // return false; // } // } // } // } // // return true; } /** * Gets the feature collection this feature is stored in. * * @return the collection that is the parent of this feature. */ public FeatureCollection<SimpleFeatureType, org.opengis.feature.simple.SimpleFeature> getParent() { return parent; } /** * Sets the parent collection this feature is stored in, if it is not * already set. If it is set then this method does nothing. * * @param collection the collection to be set as parent. */ public void setParent(FeatureCollection<SimpleFeatureType, org.opengis.feature.simple.SimpleFeature> collection) { if (parent == null) { parent = collection; } } public Feature toComplex() { try { return new ComplexWrapper(this); } catch (IllegalAttributeException iae) { throw new RuntimeException("the impossible has happened: ", iae); } } /** * This class will wrap a DefaultFeature (which is a {@link SimpleFeature} * into a Feature with multiplicity - which is to say it will always * return a List of its attributes when they are requested. These will * always be singleton Lists, since the min/max of attributes in a * DefaultFeature is 1. But this is important so that clients can deal * with all Features in the same way - always expecting Lists. * * @author Chris Holmes, Fulbright */ static final class ComplexWrapper extends DefaultFeature { /** * Private constructor to wrap the attributes in list. Could consider * making this public, but for now it seems better to keep it private * since we do no check to make sure tha attribute array isn't already * complex - and thus if it was we would wrap it in Lists again. * * @param fType DOCUMENT ME! * @param atts DOCUMENT ME! * @param fid DOCUMENT ME! * * @throws IllegalAttributeException DOCUMENT ME! */ private ComplexWrapper(DefaultFeatureType fType, Object[] atts, String fid) throws IllegalAttributeException { super(wrapValuesComplex(atts,makeComplex(fType)), makeComplex(fType), fid ); } public ComplexWrapper(DefaultFeatureType fType, Object[] atts) throws IllegalAttributeException { this(fType, atts, null); } //This could be problematic, not sure if all SimpleFeatures will have //DefaultFeatureTypes. public ComplexWrapper(SimpleFeature feature) throws IllegalAttributeException { this((DefaultFeatureType) feature.getFeatureType(), feature.getAttributes(null), feature.getID()); } /** * Sets the attribute Object at the given index. As this is a complex * feature - one with multiplicity - then all attributes passed in * must be Lists. Since this is just a wrapped SimpleFeature, the * List passed in will only ever be a singleton, but we must abide by * the contract of a ComplexFeature. * * @param index The attribute to set. * @param value A Singleton List of the attribute to set. * * @throws IllegalAttributeException If the value is not a singleton * List. * * @task REVISIT: Make List explicit in Java 1.5 * @task REVISIT: We could consider accepting none list objects, and * justify it as part of the parse method - that is to say that * most calls to setAttribute will turn the Object into the * proper form, so why not here? For a true Complex Feature this * has implications of letting people blow away their multiple * attributes, but for here there are no such dangers. -ch */ public void setAttribute(int index, Object value) { checkList(value); List valList = (List) value; int listSize = valList.size(); if (listSize == 0) { super.setAttribute(index, wrapInList(null)); } else { AttributeType type = super.getFeatureType().getAttributeType(index); Object val = valList.get(0); try { Object parsed = type.parse(val); type.validate(parsed); super.setAttribute(index, wrapInList(parsed)); } catch (IllegalArgumentException iae) { throw new IllegalAttributeException(type, val, iae); } } } private void checkList(Object value) throws IllegalAttributeException { if (value instanceof List) { List valList = (List) value; int listSize = valList.size(); if (listSize > 1) { String errMsg = "The attribute: " + valList + " has more " + "attributes (" + listSize + ") than is allowed by an " + " attributeType in a Simple Feature (1)"; throw new IllegalAttributeException(errMsg); } } else { String errMsg = "All objects set in a ComplexFeature must be " + "Lists, to account for multiplicity"; throw new IllegalAttributeException(errMsg); } } /** * Sets the attribute at the given xPath. Note that right now this * just does the name, and will fail on anything other than the name. * * @param xPath The name of the attribute to Set. * @param attribute The value to set - must be a List, for this Complex * Feature. * * @throws IllegalAttributeException DOCUMENT ME! * * @task TODO: Revisit xPath stuff - get it working or do external * implementation. */ public void setAttribute(String xPath, Object attribute) { int idx = super.getFeatureType().find(xPath); if (idx < 0) { throw new IllegalAttributeException("No attribute named " + xPath); } setAttribute(idx, attribute); } /** * Takes the raw arrayof values provided and returns them as a bunch * of Singleton Lists. * @param values * @param schema * @return */ static List wrapValuesComplex( Object[] values, DefaultFeatureType schema ) { ArrayList atts = new ArrayList(values.length); for ( int i = 0; i < values.length; i++ ) { atts.add( new AttributeImpl( wrapInList(values[i]), schema.getDescriptor(i),null) { public void setValue(Object newValue) throws IllegalArgumentException, IllegalStateException { newValue = parse(newValue); value = newValue; } protected Object parse(Object value) throws IllegalArgumentException { return value; } } ); } return atts; } protected static List wrapInList(Object attribute) { return java.util.Collections.singletonList(attribute); } protected static Object[] wrapInList(Object[] attributes, int defaultSize) { Object[] retArray = attributes; if (attributes == null) { retArray = new Object[defaultSize]; } else { retArray = attributes; } for (int i = 0; i < attributes.length; i++) { retArray[i] = wrapInList(attributes[i]); } return retArray; } protected static DefaultFeatureType makeComplex( DefaultFeatureType original ) { DefaultFeatureTypeBuilder builder = new DefaultFeatureTypeBuilder() { protected AttributeDescriptor createAttributeDescriptor(String name, final Class binding) { return new DefaultAttributeType( name, List.class, true, 1,1, null, null ){ public void validate(Object attribute) throws IllegalArgumentException { if ( !binding.isAssignableFrom(attribute.getClass())) { throw new IllegalArgumentException(); } } }; }; }; builder.setName( original.getName().getLocalPart() ); builder.setNamespaceURI(original.getName().getNamespaceURI()); for ( int i = 0; i < original.getAttributeCount(); i++ ) { AttributeType attributeType = original.getAttributeType(i); builder.add( attributeType.getLocalName(), attributeType.getBinding() ); } return (DefaultFeatureType) builder.buildFeatureType(); } } }