blob: 760a14f4e5bfb95274d51fc86711bf8d17e04ed3 [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.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.opengis.metadata.quality.DataQuality;
import org.opengis.metadata.quality.Element;
import org.opengis.metadata.quality.Result;
import org.opengis.metadata.quality.ConformanceResult;
import org.opengis.metadata.quality.QuantitativeResult;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.TestCase;
import org.junit.Test;
import static org.apache.sis.test.Assert.*;
/**
* Tests common to {@link DenseFeatureTest} and {@link SparseFeatureTest}.
*
* @author Martin Desruisseaux (Geomatys)
* @author Marc le Bihan
* @version 0.8
* @since 0.5
* @module
*/
public abstract strictfp class FeatureTestCase extends TestCase {
/**
* The feature being tested.
*/
AbstractFeature feature;
/**
* {@code true} if {@link #getAttributeValue(String)} should invoke {@link AbstractFeature#getProperty(String)},
* or {@code false} for invoking directly {@link AbstractFeature#getPropertyValue(String)}.
*/
private boolean getValuesFromProperty;
/**
* For sub-class constructors only.
*/
FeatureTestCase() {
}
/**
* Creates a new feature for the given type.
*/
abstract AbstractFeature createFeature(final DefaultFeatureType type);
/**
* Clones the {@link #feature} instance.
*/
abstract AbstractFeature cloneFeature() throws CloneNotSupportedException;
/**
* Asserts that {@link AbstractFeature#getProperty(String)} returns the given instance.
* This assertion is verified after a call to {@code AbstractFeature.setProperty(Property)}
* and should be true for all Apache SIS concrete implementations. But it is not guaranteed
* to be true for non-SIS implementations, for example built on top of {@code AbstractFeature}
* without overriding {@code setProperty(Property)}.
* Consequently, this assertion needs to be relaxed by {@link AbstractFeatureTest}.
*
* @param name the property name to check.
* @param expected the expected property instance.
* @param modified {@code true} if {@code expected} has been modified <strong>after</strong> it has been set
* to the {@link #feature} instance. Not all feature implementations can see such changes.
* @return {@code true} if the property is the expected instance, or {@code false} if it is another instance.
*/
boolean assertSameProperty(final String name, final Property expected, final boolean modified) {
assertSame(name, expected, feature.getProperty(name));
return true;
}
/**
* Returns the attribute value of the current {@link #feature} for the given name.
*/
private Object getAttributeValue(final String name) {
final Object value = feature.getPropertyValue(name);
if (getValuesFromProperty) {
/*
* Verifies consistency with the Attribute instance:
* - The AttributeType shall be the same than the one provided by FeatureType for the given name.
* - Attribute value shall be the same than the one we got at the beginning of this method.
* - Attribute values (as a collection) is either empty or contains the same value.
*/
final AbstractAttribute<?> property = (AbstractAttribute<?>) feature.getProperty(name);
assertSame(name, feature.getType().getProperty(name), property.getType());
assertSame(name, value, property.getValue());
final Collection<?> values = property.getValues();
if (value != null) {
assertSame(name, value, TestUtilities.getSingleton(values));
} else {
assertTrue(name, values.isEmpty());
}
/*
* Invoking getProperty(name) twice should return the same Property instance at least with
* Apache SIS Feature implementations. Other implementations may relax this requirement.
*/
assertSameProperty(name, property, false);
}
return value;
}
/**
* Sets the attribute of the given name to the given value.
* First, this method verifies that the previous value is equals to the given one.
* Then, this method set the attribute to the given value and check if the result.
*
* @param name the name of the attribute to set.
* @param oldValue the expected old value (may be {@code null}).
* @param newValue the new value to set.
*/
private void setAttributeValue(final String name, final Object oldValue, final Object newValue) {
assertEquals(name, oldValue, getAttributeValue(name));
feature.setPropertyValue(name, newValue);
assertEquals(name, newValue, getAttributeValue(name));
}
/**
* Tests the {@link AbstractFeature#getProperty(String)} method. This test uses a very simple and
* straightforward {@code FeatureType} similar to the ones obtained when reading a ShapeFile.
*
* <div class="note">In a previous SIS version, the first property value was always {@code null}
* if the implementation was {@link DenseFeature} (see SIS-178). This test reproduced the bug,
* and now aim to avoid regression.</div>
*
* @see <a href="https://issues.apache.org/jira/browse/SIS-178">SIS-178</a>
*/
@Test
public void testGetProperty() {
final DefaultFeatureType type = new DefaultFeatureType(
Collections.singletonMap(DefaultFeatureType.NAME_KEY, "My shapefile"), false, null,
DefaultAttributeTypeTest.attribute("COMMUNE"),
DefaultAttributeTypeTest.attribute("REF_INSEE"),
DefaultAttributeTypeTest.attribute("CODE_POSTAL"));
feature = createFeature(type);
feature.setPropertyValue("COMMUNE", "Bagneux");
feature.setPropertyValue("REF_INSEE", "92007");
feature.setPropertyValue("CODE_POSTAL", "92220");
assertEquals("CODE_POSTAL", "92220", ((AbstractAttribute) feature.getProperty("CODE_POSTAL")).getValue());
assertEquals("REF_INSEE", "92007", ((AbstractAttribute) feature.getProperty("REF_INSEE")) .getValue());
assertEquals("COMMUNE", "Bagneux", ((AbstractAttribute) feature.getProperty("COMMUNE")) .getValue());
assertEquals("CODE_POSTAL", "92220", feature.getPropertyValue("CODE_POSTAL"));
assertEquals("REF_INSEE", "92007", feature.getPropertyValue("REF_INSEE"));
assertEquals("COMMUNE", "Bagneux", feature.getPropertyValue("COMMUNE"));
}
/**
* Tests the {@link AbstractFeature#getPropertyValue(String)} method on a simple feature without super-types.
* This method:
*
* <ul>
* <li>Verifies setting attribute values.</li>
* <li>Verifies that attempts to set an attribute value of the wrong type throw an exception
* and leave the previous value unchanged.</li>
* <li>Verifies feature clone.</li>
* <li>Verifies serialization.</li>
* </ul>
*/
@Test
@DependsOnMethod("testGetProperty")
public final void testSimpleValues() {
feature = createFeature(DefaultFeatureTypeTest.city());
setAttributeValue("city", "Utopia", "Atlantide");
/*
* At this point we have the following "City" feature:
* ┌────────────┬─────────┬──────────────┬───────────┐
* │ Name │ Type │ Multiplicity │ Value │
* ├────────────┼─────────┼──────────────┼───────────┤
* │ city │ String │ [1 … 1] │ Atlantide │
* │ population │ Integer │ [1 … 1] │ │
* └────────────┴─────────┴──────────────┴───────────┘
* Verify that attempt to set an illegal value fail.
*/
try {
feature.setPropertyValue("city", 2000);
fail("Shall not be allowed to set a value of the wrong type.");
} catch (ClassCastException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("city"));
assertTrue(message, message.contains("Integer"));
}
assertEquals("Property shall not have been modified.", "Atlantide", getAttributeValue("city"));
/*
* Before we set the population attribute, the feature should be considered invalid.
* After we set it, the feature should be valid since all mandatory attributes are set.
*/
verifyQualityReports("population");
setAttributeValue("population", null, 1000);
verifyQualityReports();
/*
* Opportunist tests using the existing instance.
*/
testSerialization();
try {
testClone("population", 1000, 1500);
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
/**
* Tests the {@link AbstractFeature#getProperty(String)} method on a simple feature without super-types.
* This method also tests that attempts to set a value of the wrong type throw an exception and leave the
* previous value unchanged.
*/
@Test
@DependsOnMethod("testSimpleValues")
public void testSimpleProperties() {
getValuesFromProperty = true;
testSimpleValues();
}
/**
* Tests {@link AbstractFeature#getProperty(String)} and {@link AbstractFeature#getPropertyValue(String)}
* on a "complex" feature, involving multi-valued properties, inheritances and property overriding.
*/
@Test
@DependsOnMethod({"testSimpleValues", "testSimpleProperties"})
public void testComplexFeature() {
feature = createFeature(DefaultFeatureTypeTest.worldMetropolis());
setAttributeValue("city", "Utopia", "New York");
setAttributeValue("population", null, 8405837); // Estimation for 2013.
/*
* Set the attribute value on a property having [0 … ∞] multiplicity.
* The feature implementation should put the value in a list.
*/
assertEquals("universities", Collections.emptyList(), getAttributeValue("universities"));
feature.setPropertyValue("universities", "University of arts");
assertEquals("universities", Collections.singletonList("University of arts"), getAttributeValue("universities"));
/*
* Switch to 'getProperty' mode only after we have set at least one value,
* in order to test the conversion of existing values to property instances.
*/
getValuesFromProperty = true;
final SimpleInternationalString region = new SimpleInternationalString("State of New York");
setAttributeValue("region", null, region);
/*
* Adds more universities.
*/
@SuppressWarnings("unchecked")
final Collection<String> universities = (Collection<String>) feature.getPropertyValue("universities");
assertTrue(universities.add("University of sciences"));
assertTrue(universities.add("University of international development"));
/*
* In our 'metropolis' feature type, the region can be any CharSequence. But 'worldMetropolis'
* feature type overrides the region property with a restriction to InternationalString.
* Verifiy that this restriction is checked.
*/
try {
feature.setPropertyValue("region", "State of New York");
fail("Shall not be allowed to set a value of the wrong type.");
} catch (ClassCastException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("region"));
assertTrue(message, message.contains("String"));
}
assertSame("region", region, getAttributeValue("region"));
/*
* Before we set the 'isGlobal' attribute, the feature should be considered invalid.
* After we set it, the feature should be valid since all mandatory attributes are set.
*/
verifyQualityReports("isGlobal", "temperature");
setAttributeValue("isGlobal", null, Boolean.TRUE);
verifyQualityReports("temperature");
/*
* Opportunist tests using the existing instance.
*/
testSerialization();
try {
testClone("population", 8405837, 8405838); // A birth...
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
/**
* Tests the possibility to plugin custom attributes via {@code AbstractFeature.setProperty(Property)}.
*/
@Test
@DependsOnMethod({"testSimpleValues", "testSimpleProperties"})
public void testCustomAttribute() {
feature = createFeature(DefaultFeatureTypeTest.city());
final AbstractAttribute<String> wrong = SingletonAttributeTest.parliament();
final CustomAttribute<String> city = new CustomAttribute<>(Features.cast(
(DefaultAttributeType<?>) feature.getType().getProperty("city"), String.class));
feature.setProperty(city);
setAttributeValue("city", "Utopia", "Atlantide");
try {
feature.setProperty(wrong);
fail("Shall not be allowed to set a property of the wrong type.");
} catch (IllegalArgumentException e) {
final String message = e.getMessage();
assertTrue(message, message.contains("parliament"));
assertTrue(message, message.contains("City"));
}
if (assertSameProperty("city", city, true)) {
/*
* The quality report is expected to contains a custom element.
*/
int numOccurrences = 0;
final DataQuality quality = verifyQualityReports("population");
for (final Element report : quality.getReports()) {
final String identifier = report.getMeasureIdentification().toString();
if (identifier.equals("city")) {
numOccurrences++;
final Result result = TestUtilities.getSingleton(report.getResults());
assertInstanceOf("result", QuantitativeResult.class, result);
assertEquals("quality.report.result.errorStatistic",
CustomAttribute.ADDITIONAL_QUALITY_INFO,
String.valueOf(((QuantitativeResult) result).getErrorStatistic()));
}
}
assertEquals("Number of reports.", 1, numOccurrences);
}
}
/**
* Tests addition of values in a multi-valued property.
*/
@Test
@DependsOnMethod("testSimpleProperties")
public void testAddToCollection() {
feature = createFeature(new DefaultFeatureType(
Collections.singletonMap(DefaultFeatureType.NAME_KEY, "City"),
false, null, DefaultAttributeTypeTest.universities()));
/*
* The value below is an instance of Collection<String>. But as of Java 8, the <String> parameterized type
* can not be verified at runtime. The best check we can have is Collection<?>, which does not allow addition
* of new values.
*/
Collection<?> values = (Collection<?>) feature.getPropertyValue("universities");
assertTrue("isEmpty", values.isEmpty());
// Can not perform values.add("something") here.
feature.setPropertyValue("universities", Arrays.asList("UCAR", "Marie-Curie"));
values = (Collection<?>) feature.getPropertyValue("universities");
assertArrayEquals(new String[] {"UCAR", "Marie-Curie"}, values.toArray());
}
/**
* Asserts that {@link AbstractFeature#quality()} reports no anomaly, or only anomalies for the given properties.
*
* @param anomalousProperties the property for which we expect a report.
* @return the data quality report.
*/
private DataQuality verifyQualityReports(final String... anomalousProperties) {
int anomalyIndex = 0;
final DataQuality quality = feature.quality();
for (final Element report : quality.getReports()) {
for (final Result result : report.getResults()) {
if (result instanceof ConformanceResult && !((ConformanceResult) result).pass()) {
assertTrue("Too many reports", anomalyIndex < anomalousProperties.length);
final String propertyName = anomalousProperties[anomalyIndex];
final String identifier = report.getMeasureIdentification().toString();
final String explanation = ((ConformanceResult) result).getExplanation().toString();
assertEquals("quality.report.measureIdentification", propertyName, identifier);
assertTrue ("quality.report.result.explanation", explanation.contains(propertyName));
anomalyIndex++;
}
}
}
assertEquals("Number of reports.", anomalousProperties.length, anomalyIndex);
return quality;
}
/**
* Tests the {@link AbstractFeature#clone()} method on the current {@link #feature} instance.
* This method is invoked from other test methods using the existing feature instance in an opportunist way.
*
* @param property the name of a property to change.
* @param oldValue the old value of the given property.
* @param newValue the new value of the given property.
* @throws CloneNotSupportedException Should never happen.
*/
private void testClone(final String property, final Object oldValue, final Object newValue)
throws CloneNotSupportedException
{
final AbstractFeature clone = cloneFeature();
assertNotSame("clone", clone, feature);
assertTrue ("equals", clone.equals(feature));
assertTrue ("hashCode", clone.hashCode() == feature.hashCode());
setAttributeValue(property, oldValue, newValue);
assertEquals (property, oldValue, clone .getPropertyValue(property));
assertEquals (property, newValue, feature.getPropertyValue(property));
assertFalse ("equals", clone.equals(feature));
assertFalse ("hashCode", clone.hashCode() == feature.hashCode());
}
/**
* Tests serialization of current {@link #feature} instance.
* This method is invoked from other test methods using the existing feature instance in an opportunist way.
*/
private void testSerialization() {
assertNotSame(feature, assertSerializedEquals(feature));
}
/**
* Tests {@code equals(Object)}.
*
* @throws CloneNotSupportedException Should never happen.
*/
@Test
@DependsOnMethod("testSimpleProperties")
public void testEquals() throws CloneNotSupportedException {
feature = createFeature(DefaultFeatureTypeTest.city());
feature.setPropertyValue("city", "Tokyo");
final AbstractFeature clone = cloneFeature();
assertEquals(feature, clone);
/*
* Force the conversion of a property value into a full Property object on one and only one of
* the Features to be compared. The implementation shall be able to wrap or unwrap the values.
*/
assertEquals("Tokyo", ((AbstractAttribute) clone.getProperty("city")).getValue());
assertEquals("hashCode", feature.hashCode(), clone.hashCode());
assertEquals("equals", feature, clone);
/*
* For the other Feature instance to contain full Property object and test again.
*/
assertEquals("Tokyo", ((AbstractAttribute) feature.getProperty("city")).getValue());
assertEquals("hashCode", feature.hashCode(), clone.hashCode());
assertEquals("equals", feature, clone);
}
}