/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-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.type;
import java.util.Set;
import java.util.TreeSet;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.util.Converters;
import org.opengis.feature.Attribute;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.filter.Filter;
/**
* This is a set of utility methods used when <b>implementing</b> types.
* <p>
* This set of classes captures the all important how does it work questions,
* particularly with respect to super types.
* </p>
* FIXME: These methods need a Q&A check to confirm correct use of Super TODO:
* Cannot tell the difference in intent from FeatureTypes
*
* @author Jody Garnett, LISAsoft
* @author Justin Deoliveira, The Open Planning Project
*
*
* @source $URL$
*/
public class Types {
/**
* Ensures an attribute value is withing the restrictions of the AttributeDescriptor and
* AttributeType.
* @param attribute
* @return true if the attribute value is valid
*/
public static boolean isValid( Attribute attribute ){
try {
validate(attribute.getType(), attribute, attribute.getValue(), false );
return true;
}
catch (IllegalAttributeException invalid ){
return false;
}
}
/**
* Validates content against an attribute.
*
* @param attribute
* The attribute.
* @param attributeContent
* Content of attribute (often attribute.getValue()
*
* @throws IllegalAttributeException
* In the event that content violates any restrictions specified
* by the attribute.
*/
public static void validate(Attribute attribute, Object attributeContent)
throws IllegalAttributeException {
validate(attribute.getType(), attribute, attributeContent, false);
}
/**
*
* @param type AttributeType (often attribute.getType() )
* @param attribute Attribute being tested
* @param attributeContent Content of the attribute (often attribute.getValue() )
* @throws IllegalAttributeException
*/
public static void validate(AttributeType type, Attribute attribute, Object attributeContent)
throws IllegalAttributeException {
validate(type, attribute, attributeContent, false);
}
/**
*
* @param type AttributeType (often attribute.getType() )
* @param attribute Attribute being tested
* @param attributeContent Content of the attribute (often attribute.getValue() )
* @param isSuper True if super type is being checked
* @throws IllegalAttributeException
*/
protected static void validate(AttributeType type, Attribute attribute,
Object attributeContent, boolean isSuper) throws IllegalAttributeException {
if (type == null) {
throw new IllegalAttributeException("null type");
}
if (attributeContent == null) {
if (!attribute.isNillable()) {
throw new IllegalAttributeException(type.getName() + " not nillable");
}
return;
}
if (type.isIdentified() && attribute.getIdentifier() == null) {
throw new NullPointerException(type.getName() + " is identified, null id not accepted");
}
if (!isSuper) {
// JD: This is an issue with how the xml simpel type hierarchy
// maps to our current Java Type hiearchy, the two are inconsitent.
// For instance, xs:integer, and xs:int, the later extend the
// former, but their associated java bindings, (BigDecimal, and
// Integer)
// dont.
Class clazz = attributeContent.getClass();
Class binding = type.getBinding();
if (binding != null && binding != clazz && !binding.isAssignableFrom(clazz)) {
throw new IllegalAttributeException(clazz.getName()
+ " is not an acceptable class for " + type.getName()
+ " as it is not assignable from " + binding);
}
}
if (type.getRestrictions() != null) {
for (Filter f : type.getRestrictions()) {
if (!f.evaluate(attribute)) {
throw new IllegalAttributeException("Attribute instance (" + attribute.getIdentifier() + ")"
+ "fails to pass filter: " + f);
}
}
}
// move up the chain,
if (type.getSuper() != null) {
validate(type.getSuper(), attribute, attributeContent, true);
}
}
/**
* Ensure that attributeContent is a good value for descriptor.
*/
public static void validate(AttributeDescriptor descriptor,
Object value) throws IllegalAttributeException {
if (descriptor == null) {
throw new NullPointerException("Attribute descriptor required for validation");
}
if (value == null) {
if (!descriptor.isNillable()) {
throw new IllegalArgumentException(descriptor.getName() + " requires a non null value");
}
} else {
validate( descriptor.getType(), value, false );
}
}
/**
* Do our best to make the provided value line up with the needs of descriptor.
* <p>
* This helper method uses the Coverters api to convert the provided
* value into the required class. If the value is null (and the attribute
* is not nillable) a default value will be returned.
* @param descriptor Attribute descriptor we need to supply a value for.
* @param value The provided value
* @return Our best attempt to make a valid value
* @throws IllegalArgumentException if we really could not do it.
*/
public static Object parse(AttributeDescriptor descriptor, Object value) throws IllegalArgumentException {
if (value == null){
if( descriptor.isNillable()){
return descriptor.getDefaultValue();
}
}
else {
Class target = descriptor.getType().getBinding();
if ( !target.isAssignableFrom( value.getClass() ) ) {
// attempt to convert
Object converted = Converters.convert(value,target);
if ( converted != null ) {
return converted;
}
// else {
// throw new IllegalArgumentException( descriptor.getLocalName()+ " could not convert "+value+" into "+target);
// }
}
}
return value;
}
protected static void validate(final AttributeType type, final Object value, boolean isSuper) throws IllegalAttributeException {
if (!isSuper) {
// JD: This is an issue with how the xml simpel type hierarchy
// maps to our current Java Type hiearchy, the two are inconsitent.
// For instance, xs:integer, and xs:int, the later extend the
// former, but their associated java bindings, (BigDecimal, and
// Integer)
// dont.
Class clazz = value.getClass();
Class binding = type.getBinding();
if (binding != null && !binding.isAssignableFrom(clazz)) {
throw new IllegalAttributeException(clazz.getName()
+ " is not an acceptable class for " + type.getName()
+ " as it is not assignable from " + binding);
}
}
if (type.getRestrictions() != null && type.getRestrictions().size() > 0) {
for (Filter filter : type.getRestrictions()) {
if (!filter.evaluate(value)) {
throw new IllegalAttributeException( type.getName() + " restriction "+ filter + " not met by: " + value);
}
}
}
// move up the chain,
if (type.getSuper() != null) {
validate(type.getSuper(), value, true );
}
}
/**
* FeatureType comparison indicating if the description provided by two FeatureTypes is
* similar to the point data can be exchanged. This comparison is really very focused on the
* name / value contract and is willing to overlook details like length restrictions.
* <p>
* When creating compatible FeatureTypes you will find some systems have different abilities
* which is reflected in how well they support a given FeatureType.
* <p>
* As an example databases traditionally support variable length strings with a
* limit of 32 k; while a shapefile is limited to 256 characters. When working with data from
* both these data sources you will need to make adjustments based on these abilities.
* </p>
* If true is returned data conforming to the expected FeatureType can be used with the
* actual FeatureType.
* <p>
* After assertOrderCovered returns without error the following code will work:<pre><code>
* for( Property property : feature.getProperties() ){
* Object value = property.getValue();
*
* Property target = newFeature.getProperty( property.getName().getLocalPart() );
* target.setValue( value );
* }
* </code></pre>
* Specifically this says that between the two feature types data is assignable on a name by name
* basis.
*
* @param expected Expected FeatureType being used to compare against
* @param actual Actual FeatureType
* @return true if actual is equal to or a subset of the expected feature type.
*/
public static void assertNameAssignable( FeatureType expected, FeatureType actual){
// check feature type name
String expectedName = expected.getName().getLocalPart();
String actualName = actual.getName().getLocalPart();
if( !expectedName.equals( actualName ) ){
throw new IllegalAttributeException("Expected '"+expectedName+"' but was supplied '"+actualName+"'.");
}
// check attributes names
Set<String> names = new TreeSet<String>();
for( PropertyDescriptor descriptor : actual.getDescriptors() ){
names.add( descriptor.getName().getLocalPart() );
}
for( PropertyDescriptor descriptor : expected.getDescriptors() ){
expectedName = descriptor.getName().getLocalPart();
if( names.contains( expectedName )){
names.remove( expectedName ); // only use once!
}
else {
throw new IllegalAttributeException("Expected to find a match for '"+expectedName+"' but was not available remaining names: " + names );
}
}
if( !names.isEmpty() ){
throw new IllegalAttributeException("Expected to find attributes '"+expectedName+"' but was not available remaining names: " + names );
}
// check attribute bindings
for( PropertyDescriptor expectedDescriptor : expected.getDescriptors() ){
expectedName = expectedDescriptor.getName().getLocalPart();
PropertyDescriptor actualDescriptor = actual.getDescriptor( expectedName );
Class<?> expectedBinding = expectedDescriptor.getType().getBinding();
Class<?> actualBinding = actualDescriptor.getType().getBinding();
if( !actualBinding.isAssignableFrom( expectedBinding )){
throw new IllegalArgumentException( "Expected "+expectedBinding.getSimpleName()+" for "+expectedName+" but was "+actualBinding.getSimpleName() );
}
}
}
/**
* SimpleFeatureType comparison indicating that data from one FeatureType can
* be exchanged with another - specifically ensuring that the order / value is a
* reasonable match with the expected number of attributes on each side and the
* values correctly assignable.
* <p>
* After assertOrderCovered returns without error the following code will work:<pre><code>
* List<Object> values = feature.getAttributes();
* newFeature.setAttributes( values );
* </code></pre>
*
* @param expected
* @param actual
* @return
*/
public static void assertOrderAssignable( SimpleFeatureType expected, SimpleFeatureType actual){
// check feature type name
String expectedName = expected.getName().getLocalPart();
String actualName = actual.getName().getLocalPart();
if( !expectedName.equals( actualName ) ){
throw new IllegalAttributeException("Expected '"+expectedName+"' but was supplied '"+actualName+"'.");
}
// check attributes names
if( expected.getAttributeCount() != actual.getAttributeCount() ){
throw new IllegalAttributeException("Expected "+expected.getAttributeCount()+" attributes, but was supplied "+actual.getAttributeCount() );
}
for( int i=0; i< expected.getAttributeCount(); i++){
Class<?> expectedBinding = expected.getDescriptor(i).getType().getBinding();
Class<?> actualBinding = actual.getDescriptor(i).getType().getBinding();
if( !actualBinding.isAssignableFrom( expectedBinding )){
String name = expected.getDescriptor(i).getLocalName();
throw new IllegalArgumentException( "Expected "+expectedBinding.getSimpleName()+" for "+name+" but was "+actualBinding.getSimpleName() );
}
}
}
}