blob: 0cba383b6df591735b9a12d9a606a2102daeba67 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.feature;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Collection;
import org.opengis.util.NameFactory;
import org.opengis.util.InternationalString;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;
import org.junit.Test;
import static org.apache.sis.test.Assert.*;
import static org.apache.sis.test.TestUtilities.getSingleton;
import static java.util.Collections.singletonMap;
/**
* Tests {@link DefaultFeatureType}.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.5
* @module
*/
@DependsOn(DefaultAttributeTypeTest.class)
public final strictfp class DefaultFeatureTypeTest extends TestCase {
/**
* Convenience method returning the given name in a a property map
* to be given to {@link AbstractIdentifiedType} constructor.
*/
private static Map<String,?> name(final Object name) {
return singletonMap(AbstractIdentifiedType.NAME_KEY, name);
}
/**
* Creates a simple feature type without super-types.
* The feature contains the following attributes:
*
* <ul>
* <li>{@code city} as a {@link String} (mandatory)</li>
* <li>{@code population} as an {@link Integer} (mandatory)</li>
* </ul>
*
* @return the feature for a city.
*/
public static DefaultFeatureType city() {
final Map<String,Object> identification = new HashMap<>();
final DefaultAttributeType<String> city = DefaultAttributeTypeTest.city(identification);
final DefaultAttributeType<Integer> population = DefaultAttributeTypeTest.population(identification);
return new DefaultFeatureType(name("City"), false, null, city, population);
}
/**
* Creates a sub-type of the "city" type with only one additional property, an arbitrary amount of strings.
* The feature contains the following attribute:
*
* <ul>
* <li>{@code city} as a {@link String} (mandatory)</li>
* <li>{@code population} as an {@link Integer} (mandatory)</li>
* <li>{@code universities} as an arbitrary amount of {@link String}</li>
* </ul>
*
* @return the feature for an university city.
*/
public static DefaultFeatureType universityCity() {
return new DefaultFeatureType(name("University city"), false,
new DefaultFeatureType[] {city()}, DefaultAttributeTypeTest.universities());
}
/**
* Creates a sub-type of the "city" type with only one additional property, a string giving the parliament name.
* The feature contains the following attribute:
*
* <ul>
* <li>{@code city} as a {@link String} (mandatory)</li>
* <li>{@code population} as an {@link Integer} (mandatory)</li>
* <li>{@code parliament} as a {@link String} (mandatory)</li>
* </ul>
*
* @return the feature for a capital.
*/
public static DefaultFeatureType capital() {
return new DefaultFeatureType(name("Capital"), false,
new DefaultFeatureType[] {city()}, DefaultAttributeTypeTest.parliament());
}
/**
* Creates a sub-type of the "city" type with two additional properties.
* The feature contains the following attribute:
*
* <ul>
* <li>{@code city} as a {@link String} (mandatory)</li>
* <li>{@code population} as an {@link Integer} (mandatory)</li>
* <li>{@code region} as a {@link CharSequence} (mandatory) — the region for which the city is a metropolis.</li>
* <li>{@code isGlobal} as a {@link Boolean} (mandatory) — whether the city has an effect on global affairs.</li>
* </ul>
*
* @return the feature for a metropolis.
*/
public static DefaultFeatureType metropolis() {
final Map<String,Object> identification = new HashMap<>(4);
assertNull(identification.put(DefaultFeatureType.NAME_KEY, "Metropolis"));
assertNull(identification.put(DefaultFeatureType.NAME_KEY + "_fr", "Métropole"));
return new DefaultFeatureType(identification, false,
new DefaultFeatureType[] {city()},
new DefaultAttributeType<>(name("region"), CharSequence.class, 1, 1, null),
new DefaultAttributeType<>(name("isGlobal"), Boolean.class, 1, 1, null));
}
/**
* Creates a sub-type of the "metropolis" type with the "region" attribute overridden to
* {@link InternationalString} and an arbitrary amount of universities.
*/
static DefaultFeatureType worldMetropolis() {
return worldMetropolis(metropolis(), universityCity(), CharacteristicTypeMapTest.temperature(), InternationalString.class);
}
/**
* Creates a sub-type of the "metropolis" type with the "region" attribute overridden to the given type.
* The given type should be {@link InternationalString}, but we allow other types for testing argument checks.
*/
private static DefaultFeatureType worldMetropolis(final DefaultFeatureType metropolis,
final DefaultFeatureType universityCity, final DefaultAttributeType<?> temperature, final Class<?> regionType)
{
return new DefaultFeatureType(name("World metropolis"), false,
new DefaultFeatureType[] { // Super types
metropolis,
universityCity
},
new DefaultAttributeType<?>[] { // Properties
new DefaultAttributeType<>(name("region"), regionType, 1, 1, null),
temperature
});
}
/**
* Verifies that {@code DefaultFeatureType} methods returns unmodifiable collections.
* This method does <strong>not</strong> check recursively the properties.
*/
private static void assertUnmodifiable(final DefaultFeatureType feature) {
final Collection<?> superTypes = feature.getSuperTypes();
final Collection<?> declaredProperties = feature.getProperties(false);
final Collection<?> allProperties = feature.getProperties(true);
if (!superTypes.isEmpty()) try {
superTypes.clear();
fail("Super-types collection shall not be modifiable.");
} catch (UnsupportedOperationException e) {
assertFalse(superTypes.isEmpty());
}
if (!declaredProperties.isEmpty()) try {
declaredProperties.clear();
fail("Properties collection shall not be modifiable.");
} catch (UnsupportedOperationException e) {
assertFalse(declaredProperties.isEmpty());
}
if (!allProperties.isEmpty()) try {
allProperties.clear();
fail("Properties collection shall not be modifiable.");
} catch (UnsupportedOperationException e) {
assertFalse(allProperties.isEmpty());
}
// Opportunist check.
assertTrue("'properties(true)' shall contain all 'properties(false)' elements.",
allProperties.containsAll(declaredProperties));
}
/**
* Asserts that the given feature contains the given properties, in the same order.
* This method tests the following {@code FeatureType} methods:
*
* <ul>
* <li>{@link DefaultFeatureType#getProperties(boolean)}</li>
* <li>{@link DefaultFeatureType#getProperty(String)}</li>
* </ul>
*
* @param feature the feature to verify.
* @param includeSuperTypes {@code true} for including the properties inherited from the super-types, or
* {@code false} for returning only the properties defined explicitly in the feature type.
* @param expected names of the expected properties.
*/
private static void assertPropertiesEquals(final DefaultFeatureType feature, final boolean includeSuperTypes,
final String... expected)
{
int index = 0;
for (final AbstractIdentifiedType property : feature.getProperties(includeSuperTypes)) {
assertTrue("Found more properties than expected.", index < expected.length);
final String name = expected[index++];
assertNotNull(name, property);
assertEquals (name, property.getName().toString());
assertSame (name, property, feature.getProperty(name));
}
assertEquals("Unexpected number of properties.", expected.length, index);
try {
feature.getProperty("apple");
fail("Shall not found a non-existent property.");
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("apple"));
assertTrue(message, message.contains(feature.getName().toString()));
}
}
/**
* Tests the construction of a simple feature without super-types.
* A feature is said "simple" if the multiplicity of all attributes is [1 … 1].
*
* <p>Current implementation performs its tests on the {@link #city()} feature.</p>
*/
@Test
public void testSimple() {
final DefaultFeatureType simple = city();
assertUnmodifiable(simple);
assertEquals("name", "City", simple.getName().toString());
assertTrue ("superTypes", simple.getSuperTypes().isEmpty());
assertFalse ("isAbstract", simple.isAbstract());
assertFalse ("isSparse", simple.isSparse());
assertTrue ("isSimple", simple.isSimple());
assertTrue ("isAssignableFrom", simple.isAssignableFrom(simple));
assertEquals("instanceSize", 2, simple.indices().size());
assertPropertiesEquals(simple, false, "city", "population");
}
/**
* Tests the construction of a "complex" feature without super-types.
* A feature is said "complex" if it contains at least one attribute
* with a multiplicity different than [0 … 0] and [1 … 1].
*/
@Test
@DependsOnMethod("testSimple")
public void testComplex() {
final Map<String,Object> identification = new HashMap<>();
final DefaultAttributeType<String> city = DefaultAttributeTypeTest.city(identification);
final DefaultAttributeType<Integer> population = DefaultAttributeTypeTest.population(identification);
testComplex(city, population, 0, 0); // Simple
testComplex(city, population, 0, 1);
testComplex(city, population, 0, 2);
testComplex(city, population, 1, 2);
testComplex(city, population, 1, 1); // Simple
}
/**
* Implementation of {@link #testComplex()} for the given minimum and maximum occurrences.
*/
private static void testComplex(
final DefaultAttributeType<String> city,
final DefaultAttributeType<Integer> population,
final int minimumOccurs, final int maximumOccurs)
{
final DefaultAttributeType<String> festival = new DefaultAttributeType<>(
name("festival"), String.class, minimumOccurs, maximumOccurs, null);
final DefaultFeatureType complex = new DefaultFeatureType(
name("Festival"), false, null, city, population, festival);
assertUnmodifiable(complex);
final Collection<AbstractIdentifiedType> properties = complex.getProperties(false);
final Iterator<AbstractIdentifiedType> it = properties.iterator();
assertEquals("name", "Festival", complex.getName().toString());
assertTrue ("superTypes", complex.getSuperTypes().isEmpty());
assertTrue ("isAssignableFrom", complex.isAssignableFrom(complex));
assertFalse ("isAbstract", complex.isAbstract());
assertFalse ("isSparse", complex.isSparse());
assertEquals("isSimple", maximumOccurs == minimumOccurs, complex.isSimple());
assertEquals("instanceSize", maximumOccurs == 0 ? 2 : 3, complex.indices().size());
assertEquals("minimumOccurs", minimumOccurs, festival.getMinimumOccurs());
assertEquals("maximumOccurs", maximumOccurs, festival.getMaximumOccurs());
assertEquals("properties.size", 3, properties.size());
assertSame ("properties[0]", city, it.next());
assertSame ("properties[1]", population, it.next());
assertSame ("properties[2]", festival, it.next());
assertFalse (it.hasNext());
}
/**
* Ensures that we can not use two properties with the same name.
*/
@Test
@DependsOnMethod("testSimple")
public void testNameCollision() {
final DefaultAttributeType<String> city = new DefaultAttributeType<>(name("name"), String.class, 1, 1, null);
final DefaultAttributeType<Integer> cityId = new DefaultAttributeType<>(name("name"), Integer.class, 1, 1, null);
final DefaultAttributeType<Integer> population = new DefaultAttributeType<>(name("population"), Integer.class, 1, 1, null);
try {
final Object t = new DefaultFeatureType(name("City"), false, null, city, population, cityId);
fail("Duplicated attribute names shall not be allowed:\n" + t);
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("name")); // Property name.
assertTrue(message, message.contains("City")); // Feature name.
}
}
/**
* Same than {@link #testNameCollision()}, but resolving collisions with usage of names
* of the form {@code "head:tip"}.
*
* @since 0.6
*/
@Test
@DependsOnMethod("testNameCollision")
public void testQualifiedNames() {
final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
final DefaultAttributeType<String> city = new DefaultAttributeType<>(
name(factory.createGenericName(null, "ns1", "name")),
String.class, 1, 1, null);
final DefaultAttributeType<Integer> cityId = new DefaultAttributeType<>(
name(factory.createGenericName(null, "ns2", "name")),
Integer.class, 1, 1, null);
final DefaultAttributeType<Integer> population = new DefaultAttributeType<>(
name(factory.createGenericName(null, "ns1", "population")),
Integer.class, 1, 1, null);
final DefaultFeatureType feature = new DefaultFeatureType(
name("City"), false, null, city, cityId, population);
final Iterator<AbstractIdentifiedType> it = feature.getProperties(false).iterator();
assertSame ("properties[0]", city, it.next());
assertSame ("properties[1]", cityId, it.next());
assertSame ("properties[2]", population, it.next());
assertFalse(it.hasNext());
assertSame("Shall get from fully qualified name.", city, feature.getProperty("ns1:name"));
assertSame("Shall get from fully qualified name.", cityId, feature.getProperty("ns2:name"));
assertSame("Shall get from fully qualified name.", population, feature.getProperty("ns1:population"));
assertSame("Shall get from short alias.", population, feature.getProperty( "population"));
try {
feature.getProperty("name");
fail("Expected no alias because of ambiguity.");
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("name")); // Property name.
assertTrue(message, message.contains("ns1:name")); // Ambiguity 1.
assertTrue(message, message.contains("ns2:name")); // Ambiguity 2.
}
try {
feature.getProperty("other");
fail("Expected no property.");
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("other")); // Property name.
assertTrue(message, message.contains("City")); // Feature name.
}
}
/**
* Tests two names having the same tip, but where only one of the two names have a namespace.
*
* @since 0.8
*/
@Test
@DependsOnMethod("testQualifiedNames")
public void testQualifiedAndUnqualifiedNames() {
final NameFactory factory = DefaultFactories.forBuildin(NameFactory.class);
final DefaultAttributeType<String> a1 = new DefaultAttributeType<>(
name(factory.createGenericName(null, "sis", "identifier")),
String.class, 1, 1, null);
final DefaultAttributeType<String> a2 = new DefaultAttributeType<>(
name(factory.createGenericName(null, "identifier")),
String.class, 1, 1, null);
final DefaultFeatureType feature = new DefaultFeatureType(
name("City"), false, null, a1, a2);
assertSame("sis:identifier", a1, feature.getProperty("sis:identifier"));
assertSame( "identifier", a2, feature.getProperty( "identifier"));
}
/**
* Tests inclusion of a property of kind operation.
*/
@Test
public void testOperationProperty() {
final Map<String,?> featureName = name("Identified city");
final Map<String,?> identifierName = name("identifier");
final DefaultFeatureType[] parent = {city()};
final DefaultFeatureType city = new DefaultFeatureType(featureName, false,
parent, new LinkOperation(identifierName, parent[0].getProperty("city")));
assertPropertiesEquals(city, true, "city", "population", "identifier");
/*
* Try to add an operation that depends on a non-existent property.
* Such construction shall not be allowed.
*/
final AbstractIdentifiedType parliament = new LinkOperation(identifierName, DefaultAttributeTypeTest.parliament());
try {
final DefaultFeatureType illegal = new DefaultFeatureType(featureName, false, parent, parliament);
fail("Should not have been allowed to create this feature:\n" + illegal);
} catch (IllegalArgumentException e) {
final String message = e.getLocalizedMessage();
assertTrue(message, message.contains("identifier"));
assertTrue(message, message.contains("parliament"));
assertTrue(message, message.contains("Identified city"));
}
}
/**
* Tests a feature type which inherit from an other feature type, but without property overriding.
*
* <p>Current implementation performs its tests on the {@link #capital()} feature.</p>
*/
@Test
@DependsOnMethod({"testComplex", "testEquals"})
public void testInheritance() {
final DefaultFeatureType city = city(); // Tested by 'testSimple()'.
final DefaultFeatureType capital = capital();
assertUnmodifiable(capital);
assertEquals("name", "Capital", capital.getName().toString());
assertEquals("superTypes", city, getSingleton(capital.getSuperTypes()));
assertFalse ("isAbstract", capital.isAbstract());
assertFalse ("isSparse", capital.isSparse());
assertTrue ("isSimple", capital.isSimple());
assertEquals("instanceSize", 3, capital.indices().size());
assertPropertiesEquals(city, false, "city", "population");
assertPropertiesEquals(capital, false, "parliament");
assertPropertiesEquals(capital, true, "city", "population", "parliament");
// Check based only on name.
assertTrue ("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(city, capital));
assertFalse("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(capital, city));
// Public API.
assertTrue ("isAssignableFrom", city.isAssignableFrom(capital));
assertFalse("isAssignableFrom", capital.isAssignableFrom(city));
}
/**
* Tests the inheritance of 2 types having the same common parent.
*/
@Test
@DependsOnMethod("testInheritance")
public void testMultiInheritance() {
final DefaultFeatureType metropolis = metropolis();
final DefaultFeatureType capital = capital(); // Tested by 'testComplex()'.
final DefaultFeatureType metroCapital = new DefaultFeatureType(
name("Metropolis and capital"), false,
new DefaultFeatureType[] {metropolis, capital},
new DefaultAttributeType<>(name("country"),
String.class, 1, 1, null));
assertUnmodifiable(metroCapital);
assertEquals ("name", "Metropolis and capital", metroCapital.getName().toString());
assertArrayEquals("superTypes", new Object[] {metropolis, capital}, metroCapital.getSuperTypes().toArray());
assertFalse ("isAbstract", metroCapital.isAbstract());
assertFalse ("isSparse", metroCapital.isSparse());
assertTrue ("isSimple", metroCapital.isSimple());
assertEquals ("instanceSize", 6, metroCapital.indices().size());
assertPropertiesEquals(metroCapital, false, "country");
assertPropertiesEquals(metroCapital, true, "city", "population", "region", "isGlobal", "parliament", "country");
assertEquals("property(“region”).valueClass", CharSequence.class,
((DefaultAttributeType<?>) metroCapital.getProperty("region")).getValueClass());
// Check based only on name.
assertTrue ("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(capital, metroCapital));
assertFalse("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(metroCapital, capital));
assertTrue ("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(metropolis, metroCapital));
assertFalse("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(metroCapital, metropolis));
// Public API.
assertTrue ("isAssignableFrom", capital.isAssignableFrom(metroCapital));
assertFalse("isAssignableFrom", metroCapital.isAssignableFrom(capital));
assertTrue ("isAssignableFrom", metropolis.isAssignableFrom(metroCapital));
assertFalse("isAssignableFrom", metroCapital.isAssignableFrom(metropolis));
}
/**
* Tests inheritance with a property that override an other property with a more specific type.
*/
@Test
@DependsOnMethod({"testMultiInheritance", "testNameCollision"})
public void testPropertyOverride() {
final DefaultFeatureType metropolis = metropolis();
final DefaultFeatureType universityCity = universityCity();
final DefaultAttributeType<?> temperature = CharacteristicTypeMapTest.temperature();
try {
worldMetropolis(metropolis, universityCity, temperature, Integer.class);
fail("Shall not be allowed to override a 'CharSequence' attribute with an 'Integer' one.");
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("region"));
assertTrue(message, message.contains("Metropolis"));
}
final DefaultFeatureType worldMetropolis = worldMetropolis(metropolis, universityCity, temperature, InternationalString.class);
assertUnmodifiable(worldMetropolis);
assertEquals ("name", "World metropolis", worldMetropolis.getName().toString());
assertArrayEquals("superTypes", new Object[] {metropolis, universityCity}, worldMetropolis.getSuperTypes().toArray());
assertFalse ("isAbstract", worldMetropolis.isAbstract());
assertFalse ("isSparse", worldMetropolis.isSparse());
assertFalse ("isSimple", worldMetropolis.isSimple()); // Because of the arbitrary amount of universities.
assertEquals ("instanceSize", 6, worldMetropolis.indices().size());
assertPropertiesEquals(worldMetropolis, false, "region", "temperature");
assertPropertiesEquals(worldMetropolis, true, "city", "population", "region", "isGlobal", "universities", "temperature");
assertEquals("property(“region”).valueClass", InternationalString.class,
((DefaultAttributeType<?>) worldMetropolis.getProperty("region")).getValueClass());
// Check based only on name.
assertTrue ("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(metropolis, worldMetropolis));
assertFalse("maybeAssignableFrom", DefaultFeatureType.maybeAssignableFrom(worldMetropolis, metropolis));
// Public API.
assertTrue ("isAssignableFrom", metropolis.isAssignableFrom(worldMetropolis));
assertFalse("isAssignableFrom", worldMetropolis.isAssignableFrom(metropolis));
}
/**
* Tests the ommission of a property that duplicate a property already declared in the parent.
* This is a little bit different than {@link #testPropertyOverride()} since the duplicated property
* should be completely omitted.
*/
@Test
@DependsOnMethod("testPropertyOverride")
public void testPropertyDuplication() {
DefaultFeatureType city = city();
city = new DefaultFeatureType(name("New-City"),
false, new DefaultFeatureType[] {city()}, city.getProperty("city"));
assertPropertiesEquals(city, false);
assertPropertiesEquals(city, true, "city", "population");
}
/**
* Tests {@link DefaultFeatureType#equals(Object)}.
*/
@Test
@DependsOnMethod("testSimple")
public void testEquals() {
final DefaultFeatureType city = city();
assertTrue (city.equals(city()));
assertFalse(city.equals(capital()));
}
/**
* Tests serialization.
*/
@Test
@DependsOnMethod({"testInheritance", "testEquals"})
public void testSerialization() {
assertPropertiesEquals(assertSerializedEquals(capital()), true, "city", "population", "parliament");
}
}