| /* |
| * 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); |
| } |
| } |