blob: 36b8be37b6f8a8d0aac3b3351786505ea505b06f [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.metadata;
import java.util.Date;
import java.util.Locale;
import java.util.Random;
import java.util.Collection;
import java.util.Map;
import java.lang.reflect.Method;
import org.opengis.util.CodeList;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.CheckedContainer;
import org.apache.sis.internal.util.CollectionsExt;
import org.apache.sis.internal.metadata.Dependencies;
import org.apache.sis.test.xml.AnnotationConsistencyCheck;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.DependsOn;
import org.junit.Test;
/**
* Base class for tests done on metadata objects using reflection. This base class tests JAXB annotations
* as described in the {@link AnnotationConsistencyCheck parent class}, and tests additional aspects like:
*
* <ul>
* <li>All {@link AbstractMetadata} instance shall be initially {@linkplain AbstractMetadata#isEmpty() empty}.</li>
* <li>All getter methods shall returns a null singleton or an empty collection, never a null collection.</li>
* <li>After a call to a setter method, the getter method shall return a value equals to the given value.</li>
* </ul>
*
* This base class is defined in this {@code org.apache.sis.metadata} package because it needs to access
* package-private classes.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
@DependsOn(PropertyAccessorTest.class)
public abstract strictfp class PropertyConsistencyCheck extends AnnotationConsistencyCheck {
/**
* The standard implemented by the metadata objects to test.
*/
private final MetadataStandard standard;
/**
* Random generator, or {@code null} if not needed.
*/
private Random random;
/**
* Creates a new test suite for the given types.
*
* @param standard the standard implemented by the metadata objects to test.
* @param types the GeoAPI interfaces, {@link CodeList} or {@link Enum} types to test.
*/
protected PropertyConsistencyCheck(final MetadataStandard standard, final Class<?>... types) {
super(types);
this.standard = standard;
}
/**
* Returns the SIS implementation for the given GeoAPI interface.
*
* @return {@inheritDoc}
*/
@Override
protected <T> Class<? extends T> getImplementation(final Class<T> type) {
assertTrue(type.getName(), standard.isMetadata(type));
final Class<? extends T> impl = standard.getImplementation(type);
assertNotNull(type.getName(), impl);
return impl;
}
/**
* Return {@code true} if the given property is expected to be writable.
* If this method returns {@code false}, then {@link #testPropertyValues()}
* will not try to write a value for that property.
*
* <p>The default implementation returns {@code true}.</p>
*
* @param impl the implementation class.
* @param property the name of the property to test.
* @return {@code true} if the given property is writable.
*/
protected boolean isWritable(final Class<?> impl, final String property) {
return true;
}
/**
* Returns a dummy value of the given type. The default implementation returns values for
* {@link CharSequence}, {@link Number}, {@link Date}, {@link Locale}, {@link CodeList},
* {@link Enum} and types in the {@link #types} list.
*
* <p>The returned value may be of an other type than the given one if the
* {@code PropertyAccessor} converter method know how to convert that type.</p>
*
* @param property the name of the property for which to create a value.
* @param type the type of value to create.
* @return the value of the given {@code type}, or of a type convertible to the given type.
*/
protected Object sampleValueFor(final String property, final Class<?> type) {
if (CharSequence.class.isAssignableFrom(type)) {
return "Dummy value for " + property + '.';
}
switch (Numbers.getEnumConstant(type)) {
case Numbers.DOUBLE: return random.nextDouble() * 90;
case Numbers.FLOAT: return random.nextFloat() * 90f;
case Numbers.LONG: return (long) random.nextInt(1000000) + 1;
case Numbers.INTEGER: return random.nextInt( 10000) + 1;
case Numbers.SHORT: return (short) random.nextInt( 1000) + 1;
case Numbers.BYTE: return (byte) random.nextInt( 100) + 1;
case Numbers.BOOLEAN: return random.nextBoolean();
}
if (Date.class.isAssignableFrom(type)) {
return new Date(random.nextInt() * 1000L);
}
if (CodeList.class.isAssignableFrom(type)) try {
if (type == CodeList.class) {
return null;
}
final CodeList<?>[] codes = (CodeList<?>[]) type.getMethod("values", (Class[]) null).invoke(null, (Object[]) null);
return codes[random.nextInt(codes.length)];
} catch (ReflectiveOperationException e) {
fail(e.toString());
}
if (Locale.class.isAssignableFrom(type)) {
switch (random.nextInt(4)) {
case 0: return Locale.ROOT;
case 1: return Locale.ENGLISH;
case 2: return Locale.FRENCH;
case 3: return Locale.JAPANESE;
}
}
if (ArraysExt.containsIdentity(types, type)) {
final Class<?> impl = getImplementation(type);
if (impl != null) try {
return impl.getConstructor((Class<?>[]) null).newInstance((Object[]) null);
} catch (ReflectiveOperationException e) {
fail(e.toString());
}
}
return null;
}
/**
* Normalizes the type of the given value before comparison. In particular, {@code InternationalString}
* instances are converted to {@code String} instances.
*/
private static Object normalizeType(Object value) {
if (value instanceof CharSequence) {
value = value.toString();
}
return value;
}
/**
* Validates the given newly constructed metadata. The default implementation ensures that
* {@link AbstractMetadata#isEmpty()} returns {@code true}.
*
* @param metadata the metadata to validate.
*/
protected void validate(final AbstractMetadata metadata) {
assertTrue("AbstractMetadata.isEmpty()", metadata.isEmpty());
}
/**
* For every properties in every non-{@code Codelist} types listed in the {@link #types} array,
* tests the property values. This method verifies that all {@link AbstractMetadata} instances
* are initially {@linkplain AbstractMetadata#isEmpty() empty}, all getter methods return a null
* singleton or an empty collection, and tests setting a random value.
*/
@Test
public void testPropertyValues() {
random = TestUtilities.createRandomNumberGenerator();
for (final Class<?> type : types) {
if (!CodeList.class.isAssignableFrom(type)) {
final Class<?> impl = getImplementation(type);
if (impl != null) {
assertTrue("Not an implementation of expected interface.", type.isAssignableFrom(impl));
testPropertyValues(new PropertyAccessor(type, impl, impl));
}
}
}
}
/**
* Implementation of {@link #testPropertyValues()} for a single class.
*/
private void testPropertyValues(final PropertyAccessor accessor) {
/*
* Try to instantiate the implementation. Every implementation should
* have a no-args constructor, and their instantiation should never fail.
*/
testingClass = accessor.implementation.getCanonicalName();
final Object instance;
try {
instance = accessor.implementation.getConstructor((Class<?>[]) null).newInstance((Object[]) null);
} catch (ReflectiveOperationException e) {
fail(e.toString());
return;
}
if (instance instanceof AbstractMetadata) {
validate((AbstractMetadata) instance);
}
/*
* Iterate over all properties defined in the interface,
* and checks for the existences of a setter method.
*/
final int count = accessor.count();
for (int i=0; i<count; i++) {
testingMethod = accessor.name(i, KeyNamePolicy.METHOD_NAME);
if (skipTest(accessor.implementation, testingMethod)) {
continue;
}
final String property = accessor.name(i, KeyNamePolicy.JAVABEANS_PROPERTY);
assertNotNull("Missing method name.", testingMethod);
assertNotNull("Missing property name.", property);
assertEquals("Wrong property index.", i, accessor.indexOf(property, true));
/*
* Get the property type. In the special case where the property type
* is a collection, this is the type of elements in that collection.
*/
final Class<?> propertyType = Numbers.primitiveToWrapper(accessor.type(i, TypeValuePolicy.PROPERTY_TYPE));
final Class<?> elementType = Numbers.primitiveToWrapper(accessor.type(i, TypeValuePolicy.ELEMENT_TYPE));
assertNotNull(testingMethod, propertyType);
assertNotNull(testingMethod, elementType);
final boolean isMap = Map.class.isAssignableFrom(propertyType);
final boolean isCollection = Collection.class.isAssignableFrom(propertyType);
assertFalse("Element type can not be Collection.", Collection.class.isAssignableFrom(elementType));
assertEquals("Property and element types shall be the same if and only if not a collection.",
!(isMap | isCollection), propertyType == elementType);
/*
* Try to get a value.
*/
Object value = accessor.get(i, instance);
if (value == null) {
assertFalse("Null values are not allowed to be collections.", isMap | isCollection);
} else {
assertTrue("Wrong property type.", propertyType.isInstance(value));
if (value instanceof CheckedContainer<?>) {
assertTrue("Wrong element type in collection.",
elementType.isAssignableFrom(((CheckedContainer<?>) value).getElementType()));
}
if (isMap) {
assertTrue("Collections shall be initially empty.", ((Map<?,?>) value).isEmpty());
value = CollectionsExt.modifiableCopy((Map<?,?>) value); // Protect from changes.
} else if (isCollection) {
assertTrue("Collections shall be initially empty.", ((Collection<?>) value).isEmpty());
value = CollectionsExt.modifiableCopy((Collection<?>) value); // Protect from changes.
}
}
/*
* Try to write a value.
*/
final boolean isWritable = isWritable(accessor.implementation, property);
if (isWritable != accessor.isWritable(i)) {
fail("Non writable property: " + accessor + '.' + property);
}
if (isWritable) {
if (Date.class.isAssignableFrom(accessor.type(i, TypeValuePolicy.ELEMENT_TYPE))) {
// Dates requires sis-temporal module, which is not available for sis-metadata.
continue;
}
if (isMap) {
continue;
}
final Object newValue = sampleValueFor(property, elementType);
final Object oldValue = accessor.set(i, instance, newValue, PropertyAccessor.RETURN_PREVIOUS);
assertEquals("PropertyAccessor.set(…) shall return the value previously returned by get(…).", value, oldValue);
value = accessor.get(i, instance);
if (isCollection) {
if (newValue == null) {
assertTrue("We did not generated a random value for this type, consequently the "
+ "collection should still empty.", ((Collection<?>) value).isEmpty());
value = null;
} else {
value = TestUtilities.getSingleton((Collection<?>) value);
}
}
assertEquals("PropertyAccessor.get(…) shall return the value that we have just set.",
normalizeType(newValue), normalizeType(value));
}
}
}
/**
* Returns {@code true} if test for the given property should be skipped.
* Reasons for skipping a test are:
*
* <ul>
* <li>Class which is a union (those classes behave differently than non-union classes).</li>
* <li>Method which is the delegate of many legacy ISO 19115:2003 methods.
* Having a property that can be modified by many other properties confuse the tests.</li>
* </ul>
*/
@SuppressWarnings("deprecation")
private static boolean skipTest(final Class<?> implementation, final String method) {
return implementation == org.apache.sis.metadata.iso.maintenance.DefaultScopeDescription.class ||
(implementation == org.apache.sis.metadata.iso.citation.DefaultResponsibleParty.class &&
method.equals("getParties"));
}
/**
* Verifies the {@link TitleProperty} annotations. This method verifies that the property exist,
* is a singleton, and is not another metadata object. The property should also be mandatory,
* but this method does not verify that restriction since there is some exceptions.
*
* @since 0.8
*/
@Test
public void testTitlePropertyAnnotation() {
for (final Class<?> type : types) {
final Class<?> impl = standard.getImplementation(type);
if (impl != null) {
final TitleProperty an = impl.getAnnotation(TitleProperty.class);
if (an != null) {
final String name = an.name();
final String message = impl.getSimpleName() + '.' + name;
final PropertyAccessor accessor = new PropertyAccessor(type, impl, impl);
// Property shall exist.
final int index = accessor.indexOf(name, false);
assertTrue(message, index >= 0);
// Property can not be a metadata.
final Class<?> elementType = accessor.type(index, TypeValuePolicy.ELEMENT_TYPE);
assertFalse(message, standard.isMetadata(elementType));
// Property shall be a singleton.
assertSame(message, elementType, accessor.type(index, TypeValuePolicy.PROPERTY_TYPE));
}
}
}
}
/**
* Verifies the {@link Dependencies} annotations. This method verifies that the annotation is applied on
* deprecated getter methods, that the referenced properties exist and the getter methods of referenced
* properties are not deprecated.
*
* @throws NoSuchMethodException if {@link PropertyAccessor} references a non-existent method (would be a bug).
*
* @since 0.8
*/
@Test
public void testDependenciesAnnotation() throws NoSuchMethodException {
for (final Class<?> type : types) {
final Class<?> impl = standard.getImplementation(type);
if (impl != null) {
Map<String,String> names = null;
for (final Method method : impl.getDeclaredMethods()) {
final Dependencies dep = method.getAnnotation(Dependencies.class);
if (dep != null) {
final String name = method.getName();
if (names == null) {
names = standard.asNameMap(type, KeyNamePolicy.JAVABEANS_PROPERTY, KeyNamePolicy.METHOD_NAME);
}
/*
* Currently, @Dependencies is applied only on deprecated getter methods.
* However this policy may change in future Apache SIS versions.
*/
assertTrue(name, name.startsWith("get"));
assertTrue(name, method.isAnnotationPresent(Deprecated.class));
/*
* All dependencies shall be non-deprecated methods. Combined with above
* restriction about @Dependencies applied only on deprected methods, this
* ensure that there is no cycle.
*/
for (final String ref : dep.value()) {
// Verify that the dependency is a property name.
assertEquals(name, names.get(ref), ref);
// Verify that the referenced method is non-deprecated.
assertFalse(name, impl.getMethod(names.get(ref)).isAnnotationPresent(Deprecated.class));
}
}
}
}
}
}
}