blob: 76c10323e7db898aac1eba43b98653a7d66b7a2b [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.test.xml;
import java.util.Set;
import java.util.HashSet;
import java.util.Objects;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.xml.XMLConstants;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlSchema;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlRootElement;
import org.opengis.annotation.UML;
import org.opengis.annotation.Classifier;
import org.opengis.annotation.Stereotype;
import org.opengis.annotation.Obligation;
import org.opengis.annotation.Specification;
import org.opengis.util.CodeList;
import org.opengis.util.ControlledVocabulary;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.xml.Namespaces;
import org.apache.sis.internal.xml.Schemas;
import org.apache.sis.internal.xml.LegacyNamespaces;
import org.apache.sis.test.DependsOnMethod;
import org.apache.sis.test.TestUtilities;
import org.apache.sis.test.TestCase;
import org.junit.Test;
import junit.framework.AssertionFailedError;
/**
* Verifies consistency between {@link UML}, {@link XmlElement} and other annotations.
* Some tests performed by this class are:
*
* <ul>
* <li>All implementation classes have {@link XmlRootElement} and {@link XmlType} annotations.</li>
* <li>The name declared in the {@code XmlType} annotations matches the
* {@link #getExpectedXmlTypeName expected value}.</li>
* <li>The name declared in the {@code XmlRootElement} (classes) or {@link XmlElement} (methods)
* annotations matches the identifier declared in the {@link UML} annotation of the GeoAPI interfaces.
* The UML - XML name mapping can be changed by overriding {@link #getExpectedXmlElementName(Class, UML)}.</li>
* <li>The {@code XmlElement.required()} boolean is consistent with the UML {@linkplain Obligation obligation}.</li>
* <li>The namespace declared in the {@code XmlRootElement} or {@code XmlElement} annotations
* is not redundant with the {@link XmlSchema} annotation in the package.</li>
* <li>The prefixes declared in the {@link XmlNs} annotations match the
* {@linkplain Namespaces#getPreferredPrefix expected prefixes}.</li>
* <li>The {@linkplain #getWrapperFor wrapper}, if any, is consistent.</li>
* </ul>
*
* This class does not verify JAXB annotations against a XSD file.
* For such verification, see {@link SchemaCompliance}.
*
* @author Cédric Briançon (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
public abstract strictfp class AnnotationConsistencyCheck extends TestCase {
/**
* The {@value} string used in JAXB annotations for default names or namespaces.
*/
static final String DEFAULT = "##default";
/**
* The GeoAPI interfaces, {@link CodeList} or {@link Enum} types to test.
* This array is specified at construction time. Each test iterates over
* all types in this array.
*/
protected final Class<?>[] types;
/**
* The type being tested, or {@code null} if none. In case of test failure, this information
* will be used by the {@code assert(…)} methods for formatting a message giving the name of
* class and method where the failure occurred.
*
* @see #fail(String)
*/
protected String testingClass;
/**
* The method being tested, or {@code null} if none. In case of test failure, this information
* will be used by the {@code assert(…)} methods for formatting a message giving the name of
* class and method where the failure occurred.
*
* @see #fail(String)
*/
protected String testingMethod;
/**
* Creates a new test suite for the given types.
* The given sequence of types is assigned to the {@link #types} field.
*
* @param types the GeoAPI interfaces, {@link CodeList} or {@link Enum} types to test.
*/
protected AnnotationConsistencyCheck(final Class<?>... types) {
this.types = types; // No need to clone — test classes are normally used only by SIS.
}
/**
* Returns the SIS implementation class for the given GeoAPI interface.
* For example the implementation of the {@link org.opengis.metadata.citation.Citation}
* interface is the {@link org.apache.sis.metadata.iso.citation.DefaultCitation} class.
*
* @param <T> the type represented by the {@code type} argument.
* @param type the GeoAPI interface (never a {@link CodeList} or {@link Enum} type).
* @return the SIS implementation for the given interface.
*/
protected abstract <T> Class<? extends T> getImplementation(Class<T> type);
/**
* If the given GeoAPI type, when marshalled to XML, is wrapped into an other XML element,
* returns the class of the wrapper for that XML element. Otherwise returns {@code null}.
* Such wrappers are unusual in XML (except for lists), but the ISO 19115-3 standard do that
* systematically for every elements.
*
* <p><b>Example:</b> when a {@link org.apache.sis.metadata.iso.citation.DefaultContact}
* is marshalled to XML inside a {@code ResponsibleParty}, the element is not marshalled
* directly inside its parent as we usually do in XML. Instead, we have a {@code <CI_Contact>}.
* inside the {@code <contactInfo>} element as below:</p>
*
* {@preformat xml
* <CI_ResponsibleParty>
* <contactInfo>
* <CI_Contact>
* ...
* </CI_Contact>
* </contactInfo>
* </CI_ResponsibleParty>
* }
*
* To reflect that fact, this method shall return the internal {@code CI_Contact}
* wrapper class for the {@link org.apache.sis.metadata.iso.citation.DefaultCitation} argument.
* If no wrapper is expected for the given class, then this method shall return {@code null}.
*
* <p>If a wrapper is expected for the given class but was not found, then this method shall throw
* {@link ClassNotFoundException}. Note that no wrapper may be defined explicitly for the given type,
* while a wrapper is defined for a parent of the given type. This method does not need to care about
* such situation, since the caller will automatically searches for a parent class if
* {@code ClassNotFoundException} has been thrown.</p>
*
* <p>In SIS implementation, most wrappers are also {@link javax.xml.bind.annotation.adapters.XmlAdapter}.
* But this is not a requirement.</p>
*
* @param type the GeoAPI interface, {@link CodeList} or {@link Enum} type.
* @return the wrapper for the given type, or {@code null} if none.
* @throws ClassNotFoundException if a wrapper was expected but not found.
*/
protected abstract Class<?> getWrapperFor(Class<?> type) throws ClassNotFoundException;
/**
* The value returned by {@link #getWrapperFor(Class)}, together with a boolean telling
* whether the wrapper has been found in the tested class or in one of its parent classes.
*/
private static final class WrapperClass {
final Class<?> type;
boolean isInherited;
WrapperClass(final Class<?> type) {
this.type = type;
}
}
/**
* Returns the value of {@link #getWrapperFor(Class)} for the given class, or for a parent
* of the given class if {@code getWrapperFor(Class)} threw {@code ClassNotFoundException}.
*
* @param type the GeoAPI interface, {@link CodeList} or {@link Enum} type.
* @return the wrapper for the given type.
* {@link WrapperClass#type} is {@code null} if no wrapper has been found.
* @throws ClassNotFoundException if a wrapper was expected but not found in the
* given type neither in any of the parent classes.
*/
private WrapperClass getWrapperInHierarchy(final Class<?> type) throws ClassNotFoundException {
try {
return new WrapperClass(getWrapperFor(type));
} catch (ClassNotFoundException e) {
for (final Class<?> parent : type.getInterfaces()) {
if (ArraysExt.containsIdentity(types, parent)) try {
final WrapperClass wrapper = getWrapperInHierarchy(parent);
wrapper.isInherited = true;
return wrapper;
} catch (ClassNotFoundException e2) {
e.addSuppressed(e2);
}
}
throw e;
}
}
/**
* Returns the identifier specified by the given UML, taking only the first one if it compound.
* For example if the identifier is {@code "defaultLocale+otherLocale"}, then this method returns
* only {@code "defaultLocale"}.
*/
private static String firstIdentifier(final UML uml) {
String identifier = uml.identifier();
final int s = identifier.indexOf('+');
return (s >= 0) ? identifier.substring(0, s) : identifier;
}
/**
* Returns the beginning of expected namespace for an element defined by the given UML.
* For example the namespace of most types defined by {@link Specification#ISO_19115}
* starts with is {@code "http://standards.iso.org/iso/19115/-3/"}.
*
* <p>The default implementation recognizes the
* {@linkplain Specification#ISO_19115 ISO 19115},
* {@linkplain Specification#ISO_19115_2 ISO 19115-2},
* {@linkplain Specification#ISO_19115_3 ISO 19115-3},
* {@linkplain Specification#ISO_19139 ISO 19139} and
* {@linkplain Specification#ISO_19108 ISO 19108} specifications,
* with a hard-coded list of exceptions to the general rule.
* Subclasses shall override this method if they need to support more namespaces.</p>
*
* <p>Note that a more complete verification is done by {@link SchemaCompliance}.
* But the test done in this {@link AnnotationConsistencyCheck} class can be run without network access.</p>
*
* <p>The prefix for the given namespace will be fetched by
* {@link Namespaces#getPreferredPrefix(String, String)}.</p>
*
* @param impl the implementation class ({@link CodeList} or {@link Enum} type).
* @param uml the UML associated to the class or the method.
* @return the expected namespace.
* @throws IllegalArgumentException if the given UML is unknown to this method.
*/
@SuppressWarnings("deprecation")
protected String getExpectedNamespaceStart(final Class<?> impl, final UML uml) {
final String identifier = uml.identifier();
switch (identifier) {
case "SV_CoupledResource":
case "SV_OperationMetadata":
case "SV_OperationChainMetadata":
case "SV_ServiceIdentification": { // Historical reasons (other standard integrated into ISO 19115)
assertEquals("Unexpected @Specification value.", Specification.ISO_19115, uml.specification());
assertEquals("Specification version should be latest ISO 19115.", (short) 0, uml.version());
return Namespaces.SRV;
}
case "DQ_TemporalAccuracy": // Renamed DQ_TemporalQuality
case "DQ_NonQuantitativeAttributeAccuracy": { // Renamed DQ_NonQuantitativeAttributeCorrectness
assertEquals("Unexpected @Specification value.", Specification.ISO_19115, uml.specification());
assertEquals("Specification version should be legacy ISO 19115.", (short) 2003, uml.version());
return LegacyNamespaces.GMD;
}
case "role": {
if (org.opengis.metadata.citation.ResponsibleParty.class.isAssignableFrom(impl)) {
return LegacyNamespaces.GMD; // Override a method defined in Responsibility
}
break;
}
case "lineage": {
if (org.opengis.metadata.quality.DataQuality.class.isAssignableFrom(impl)) {
return LegacyNamespaces.GMD; // Deprecated property in a type not yet upgraded.
}
break;
}
case "errorStatistic": {
if (org.opengis.metadata.quality.QuantitativeResult.class.isAssignableFrom(impl)) {
return LegacyNamespaces.GMD; // Deprecated property in a type not yet upgraded.
}
break;
}
case "nameOfMeasure":
case "measureIdentification":
case "measureDescription":
case "evaluationMethodType":
case "evaluationMethodDescription":
case "evaluationProcedure": {
if (org.opengis.metadata.quality.Element.class.isAssignableFrom(impl)) {
return LegacyNamespaces.GMD; // Deprecated property in a type not yet upgraded.
}
break;
}
case "dateTime": {
if (org.opengis.metadata.quality.Element.class.isAssignableFrom(impl)) {
return Namespaces.GMD;
}
break;
}
case "fileFormat": {
if (org.opengis.metadata.distribution.DataFile.class.isAssignableFrom(impl)) {
return LegacyNamespaces.GMX; // Deprecated method (removed from ISO 19115-3:2016)
}
break;
}
}
/*
* GeoAPI has not yet been upgraded to ISO 19157. Interfaces in the "org.opengis.metadata.quality"
* package are still defined according the old specification. Those types have the "DQ_" or "QE_"
* prefix. This issue applies also to properties (starting with a lower case).
*/
if (identifier.startsWith("DQ_")) {
assertEquals("Unexpected @Specification value.", Specification.ISO_19115, uml.specification());
assertEquals("Specification version should be legacy ISO 19115.", (short) 2003, uml.version());
return Namespaces.MDQ;
}
if (identifier.startsWith("QE_")) {
assertEquals("Unexpected @Specification value.", Specification.ISO_19115_2, uml.specification());
switch (uml.version()) {
case 0: return Namespaces.MDQ;
case 2009: return LegacyNamespaces.GMI;
default: fail("Unexpected version number in " + uml);
}
}
if (org.opengis.metadata.quality.DataQuality.class.isAssignableFrom(impl) || // For properties in those types.
org.opengis.metadata.quality.Element.class.isAssignableFrom(impl) ||
org.opengis.metadata.quality.Result.class.isAssignableFrom(impl))
{
return Namespaces.MDQ;
}
/*
* General cases (after we processed all the special cases)
* based on which standard defines the type or property.
*/
if (uml.version() != 0) {
switch (uml.specification()) {
case ISO_19115: return LegacyNamespaces.GMD;
case ISO_19115_2: return LegacyNamespaces.GMI;
}
}
switch (uml.specification()) {
case ISO_19115:
case ISO_19115_2:
case ISO_19115_3: return Schemas.METADATA_ROOT;
case ISO_19139: return LegacyNamespaces.GMX;
case ISO_19108: return LegacyNamespaces.GMD;
default: throw new IllegalArgumentException(uml.toString());
}
}
/**
* Returns the name of the XML type for an interface described by the given UML.
* For example in ISO 19115-3, the XML type of {@code CI_Citation} is {@code CI_Citation_Type}.
* The default implementation returns {@link UML#identifier()}, possibly with {@code "Abstract"} prepended,
* and unconditionally with {@code "_Type"} appended.
* Subclasses shall override this method when mismatches are known to exist between the UML and XML type names.
*
* @param stereotype the stereotype of the interface, or {@code null} if none.
* @param uml the UML of the interface for which to get the corresponding XML type name.
* @return the name of the XML type for the given element, or {@code null} if none.
*
* @see #testImplementationAnnotations()
*/
protected String getExpectedXmlTypeName(final Stereotype stereotype, final UML uml) {
final String rootName = uml.identifier();
final StringBuilder buffer = new StringBuilder(rootName.length() + 13);
if (Stereotype.ABSTRACT.equals(stereotype)) {
buffer.append("Abstract");
}
return buffer.append(rootName).append("_Type").toString();
}
/**
* Returns the name of the XML root element for an interface described by the given UML.
* The default implementation returns {@link UML#identifier()}, possibly with {@code "Abstract"} prepended.
* Subclasses shall override this method when mismatches are known to exist between the UML and XML element names.
*
* @param stereotype the stereotype of the interface, or {@code null} if none.
* @param uml the UML of the interface for which to get the corresponding XML root element name.
* @return the name of the XML root element for the given UML.
*
* @see #testImplementationAnnotations()
*/
protected String getExpectedXmlRootElementName(final Stereotype stereotype, final UML uml) {
String name = uml.identifier();
if (Stereotype.ABSTRACT.equals(stereotype)) {
name = "Abstract".concat(name);
}
return name;
}
/**
* Returns the name of the XML element for a method described by the given UML.
* This method is invoked for a property (field or method) defined by an interface.
* The {@code enclosing} argument is the interface containing the property.
*
* <p>The default implementation returns {@link UML#identifier()}. Subclasses shall override this method
* when mismatches are known to exist between the UML and XML element names.</p>
*
* @param enclosing the GeoAPI interface which contains the property, or {@code null} if none.
* @param uml the UML element for which to get the corresponding XML element name.
* @return the XML element name for the given UML element.
*
* @see #testMethodAnnotations()
*/
protected String getExpectedXmlElementName(final Class<?> enclosing, final UML uml) {
String name = firstIdentifier(uml);
switch (name) {
case "stepDateTime": {
if (org.opengis.metadata.lineage.ProcessStep.class.isAssignableFrom(enclosing)) {
name = "dateTime";
}
break;
}
case "satisfiedPlan": {
if (org.opengis.metadata.acquisition.Requirement.class.isAssignableFrom(enclosing)) {
name = "satisifiedPlan"; // Misspelling in ISO 19115-3:2016
}
break;
}
case "meteorologicalConditions": {
if (org.opengis.metadata.acquisition.EnvironmentalRecord.class.isAssignableFrom(enclosing)) {
name = "meterologicalConditions"; // Misspelling in ISO 19115-3:2016
}
break;
}
case "valueType": {
if (org.opengis.metadata.quality.Result.class.isAssignableFrom(enclosing)) {
return "valueRecordType";
}
break;
}
}
return name;
}
/**
* Replaces {@value #DEFAULT} value by the {@link XmlSchema} namespace if needed,
* then performs validity check on the resulting namespace. This method checks that:
*
* <ul>
* <li>The namespace is not redundant with the package-level {@link XmlSchema} namespace.</li>
* <li>The namespace is declared in a package-level {@link XmlNs} annotation.</li>
* <li>The namespace starts with the {@linkplain #getExpectedNamespaceStart expected namespace}.</li>
* </ul>
*
* @param namespace the namespace given by the {@code @XmlRootElement} or {@code @XmlElement} annotation.
* @param impl the implementation or wrapper class from which to get the package namespace.
* @param uml the {@code @UML} annotation, or {@code null} if none.
* @return the actual namespace (same as {@code namespace} if it was not {@value #DEFAULT}).
*/
private String assertExpectedNamespace(String namespace, final Class<?> impl, final UML uml) {
assertNotNull("Missing namespace.", namespace);
assertFalse("Missing namespace.", namespace.trim().isEmpty());
/*
* Get the namespace declared at the package level, and ensure the the
* given namespace is not redundant with that package-level namespace.
*/
final XmlSchema schema = impl.getPackage().getAnnotation(XmlSchema.class);
assertNotNull("Missing @XmlSchema annotation in package-info.", schema);
final String schemaNamespace = schema.namespace(); // May be XMLConstants.NULL_NS_URI
assertFalse("Namespace declaration is redundant with package-info @XmlSchema.", namespace.equals(schemaNamespace));
/*
* Resolve the namespace given in argument: using the class-level namespace if needed,
* or the package-level namespace if the class-level one is not defined.
*/
if (DEFAULT.equals(namespace)) {
final XmlType type = impl.getAnnotation(XmlType.class);
if (type == null || DEFAULT.equals(namespace = type.namespace())) {
namespace = schemaNamespace;
}
assertFalse("No namespace defined.", XMLConstants.NULL_NS_URI.equals(namespace));
}
/*
* Check that the namespace is declared in the package-level @XmlNs annotation.
* We do not verify the validity of those @XmlNs annotations, since this is the
* purpose of the 'testPackageAnnotations()' method.
*/
boolean found = false;
for (final XmlNs ns : schema.xmlns()) {
if (namespace.equals(ns.namespaceURI())) {
found = true;
break;
}
}
if (!found) {
fail("Namespace for " + impl + " is not declared in the package @XmlSchema.xmlns().");
}
/*
* Check that the namespace is one of the namespaces controlled by the specification.
* We check only the namespace start, since some specifications define many namespaces
* under a common root (e.g. "http://standards.iso.org/iso/19115/-3/").
*/
if (uml != null && false) { // This verification is available only on development branches.
final String expected = getExpectedNamespaceStart(impl, uml);
if (!namespace.startsWith(expected)) {
fail("Expected " + expected + "… namespace for that ISO specification but got " + namespace);
}
}
return namespace;
}
/**
* Returns the namespace declared in the {@link XmlSchema} annotation of the given package,
* or {@code null} if none.
*
* @param p the package, or {@code null}.
* @return the namespace, or {@code null} if none.
*/
private static String getNamespace(final Package p) {
if (p != null) {
final XmlSchema schema = p.getAnnotation(XmlSchema.class);
if (schema != null) {
final String namespace = schema.namespace().trim();
if (!namespace.isEmpty() && !DEFAULT.equals(namespace)) {
return namespace;
}
}
}
return null;
}
/**
* Returns the namespace declared in the {@link XmlRootElement} annotation of the given class,
* or the package annotation if none is found in the class.
*
* @param impl the implementation class, or {@code null}.
* @return the namespace, or {@code null} if none.
*/
private static String getNamespace(final Class<?> impl) {
if (impl == null) {
return null;
}
final XmlRootElement root = impl.getAnnotation(XmlRootElement.class);
if (root != null) {
final String namespace = root.namespace().trim();
if (!namespace.isEmpty() && !DEFAULT.equals(namespace)) {
return namespace;
}
}
return getNamespace(impl.getPackage());
}
/**
* Returns {@code true} if the given method is public from a GeoAPI point of view.
*/
private static boolean isPublic(final Method method) {
return (method.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) != 0;
}
/**
* Returns {@code true} if the given method should be ignored,
* either because it is a standard method from the JDK or because it is a non-standard extension.
* If {@code true}, then {@code method} does not need to have {@link UML} or {@link XmlElement} annotation.
*
* @param method the method to verify.
* @return {@code true} if the given method should be ignored, or {@code false} otherwise.
*
* @since 0.5
*/
protected boolean isIgnored(final Method method) {
switch (method.getName()) {
/*
* Spelling changed.
*/
case "getCenterPoint": {
return true;
}
/*
* Method that override an annotated method in parent class.
*/
case "getUnits": {
return org.opengis.metadata.content.Band.class.isAssignableFrom(method.getDeclaringClass());
}
/*
* Types for which JAXB binding has not yet implemented.
*/
case "getGeographicCoordinates": {
return org.opengis.metadata.spatial.GCP.class.isAssignableFrom(method.getDeclaringClass());
}
/*
* GeoAPI extension for inter-operability with JDK API, not defined in ISO specification.
*/
case "getCurrency": {
return org.opengis.metadata.distribution.StandardOrderProcess.class.isAssignableFrom(method.getDeclaringClass());
}
/*
* ISO 19115-2 properties moved from MI_Band to MD_SampleDimension by GeoAPI.
* Must be taken in account only when checking the Band subtype.
*/
case "getNominalSpatialResolution":
case "getTransferFunctionType": {
final Class<?> dc = method.getDeclaringClass();
return org.opengis.metadata.content.SampleDimension.class.isAssignableFrom(dc)
&& !org.opengis.metadata.content.Band.class.isAssignableFrom(dc);
}
/*
* Standard Java methods overridden in some GeoAPI interfaces for Javadoc purposes.
*/
case "equals":
case "hashCode":
case "doubleValue": return true;
}
return false;
}
/**
* Tests the annotations on every GeoAPI interfaces and code lists in the {@link #types} array.
* More specifically this method tests that:
*
* <ul>
* <li>All elements in {@link #types} except code lists are interfaces.</li>
* <li>All elements in {@code types} have a {@link UML} annotation.</li>
* <li>All methods except deprecated methods and methods overriding JDK methods
* have a {@link UML} annotation.</li>
* </ul>
*/
@Test
public void testInterfaceAnnotations() {
for (final Class<?> type : types) {
testingMethod = null;
testingClass = type.getCanonicalName();
UML uml = type.getAnnotation(UML.class);
assertNotNull("Missing @UML annotation.", uml);
if (!ControlledVocabulary.class.isAssignableFrom(type)) {
for (final Method method : type.getDeclaredMethods()) {
if (isPublic(method)) {
testingMethod = method.getName();
if (!isIgnored(method)) {
uml = method.getAnnotation(UML.class);
if (!method.isAnnotationPresent(Deprecated.class)) {
assertNotNull("Missing @UML annotation.", uml);
}
}
}
}
}
}
}
/**
* Tests the annotations in the {@code package-info} files of Apache SIS implementations of the
* interfaces enumerated in the {@code #types} array. More specifically this method tests that:
*
* <ul>
* <li>The prefixes declared in the {@link XmlNs} annotations match the
* {@linkplain Namespaces#getPreferredPrefix expected prefixes}.</li>
* </ul>
*/
@Test
public void testPackageAnnotations() {
final Set<Package> packages = new HashSet<>();
for (final Class<?> type : types) {
if (!ControlledVocabulary.class.isAssignableFrom(type)) {
testingClass = type.getCanonicalName();
final Class<?> impl = getImplementation(type);
if (impl != null) {
testingClass = impl.getCanonicalName();
final Package p = impl.getPackage();
assertNotNull("Missing package information.", p);
packages.add(p);
}
}
}
for (final Package p : packages) {
for (final XmlNs ns : p.getAnnotation(XmlSchema.class).xmlns()) {
testingClass = p.getName();
final String namespace = ns.namespaceURI();
assertEquals("Unexpected namespace prefix.", Namespaces.getPreferredPrefix(namespace, null), ns.prefix());
}
}
}
/**
* Tests the annotations on every SIS implementations of the interfaces enumerated
* in the {@link #types} array. More specifically this method tests that:
*
* <ul>
* <li>All implementation classes have {@link XmlRootElement} and {@link XmlType} annotations.</li>
* <li>The name declared in the {@code XmlType} annotations matches the
* {@link #getExpectedXmlTypeName expected value}.</li>
* <li>The name declared in the {@code XmlRootElement} annotations matches the identifier declared
* in the {@link UML} annotation of the GeoAPI interfaces, with {@code "Abstract"} prefix added
* if needed.</li>
* <li>The namespace declared in the {@code XmlRootElement} annotations is not redundant with
* the {@link XmlSchema} annotation in the package.</li>
* </ul>
*
* This method does not check the method annotations, since it is {@link #testMethodAnnotations()} job.
*/
@Test
@DependsOnMethod("testInterfaceAnnotations")
public void testImplementationAnnotations() {
for (final Class<?> type : types) {
if (ControlledVocabulary.class.isAssignableFrom(type)) {
// Skip code lists, since they are not the purpose of this test.
continue;
}
testingClass = type.getCanonicalName();
/*
* Get the implementation class, which is mandatory (otherwise the
* subclass shall not include the interface in the 'types' array).
*/
final Class<?> impl = getImplementation(type);
assertNotNull("No implementation found.", impl);
assertNotSame("No implementation found.", type, impl);
testingClass = impl.getCanonicalName();
/*
* Compare the XmlRootElement with the UML annotation, if any. The UML annotation
* is mandatory in the default implementation of the 'testInterfaceAnnotations()'
* method, but we don't require the UML to be non-null here since this is not the
* job of this test method. This is because subclasses may choose to override the
* 'testInterfaceAnnotations()' method.
*/
final XmlRootElement root = impl.getAnnotation(XmlRootElement.class);
assertNotNull("Missing @XmlRootElement annotation.", root);
final UML uml = type.getAnnotation(UML.class);
Stereotype stereotype = null;
if (uml != null) {
final Classifier c = type.getAnnotation(Classifier.class);
if (c != null) {
stereotype = c.value();
}
assertEquals("Wrong @XmlRootElement.name().", getExpectedXmlRootElementName(stereotype, uml), root.name());
}
/*
* Check that the namespace is the expected one (according subclass)
* and is not redundant with the package @XmlSchema annotation.
*/
assertExpectedNamespace(root.namespace(), impl, uml);
/*
* Compare the XmlType annotation with the expected value.
*/
final XmlType xmlType = impl.getAnnotation(XmlType.class);
assertNotNull("Missing @XmlType annotation.", xmlType);
String expected = getExpectedXmlTypeName(stereotype, uml);
if (expected == null) {
expected = DEFAULT;
}
assertEquals("Wrong @XmlType.name().", expected, xmlType.name());
}
}
/**
* Tests the annotations on every methods of SIS classes.
* More specifically this method tests that:
*
* <ul>
* <li>The name declared in {@link XmlElement} matches the UML identifier.</li>
* <li>The {@code XmlElement.required()} boolean is consistent with the UML {@linkplain Obligation obligation}.</li>
* <li>The namespace declared in {@code XmlElement} is not redundant with the one declared in the package.</li>
* </ul>
*/
@Test
@DependsOnMethod("testImplementationAnnotations")
public void testMethodAnnotations() {
for (final Class<?> type : types) {
if (ControlledVocabulary.class.isAssignableFrom(type)) {
// Skip code lists, since they are not the purpose of this test.
continue;
}
testingMethod = null;
testingClass = type.getCanonicalName();
final Class<?> impl = getImplementation(type);
if (impl == null) {
/*
* Implementation existence are tested by 'testImplementationAnnotations()'.
* It is not the purpose of this test to verify again their existence.
*/
continue;
}
testingClass = impl.getCanonicalName();
for (final Method method : type.getDeclaredMethods()) {
if (!isPublic(method) || isIgnored(method)) {
continue;
}
testingMethod = method.getName();
final UML uml = method.getAnnotation(UML.class);
XmlElement element;
try {
element = impl.getMethod(testingMethod).getAnnotation(XmlElement.class);
} catch (NoSuchMethodException e) {
fail(e.toString());
continue;
}
if (element == null) {
if (uml == null) {
continue;
}
/*
* If the method does not have a @XmlElement annotation, search for a private method having the
* @XmlElement annotation with expected name. This situation happens when metadata object needs
* to perform some extra step at XML marshalling time only (not when using directly the API),
* for example verifying whether we are marshalling ISO 19139:2007 or ISO 19115-3:2016.
*/
boolean wasPublic = false;
final String identifier = firstIdentifier(uml);
for (final Method pm : impl.getDeclaredMethods()) {
final XmlElement e = pm.getAnnotation(XmlElement.class);
if (e != null && identifier.equals(e.name())) {
final boolean isPublic = isPublic(pm);
if (element != null) {
if (isPublic & !wasPublic) continue; // Give precedence to private methods.
if (isPublic == wasPublic) {
fail("Duplicated @XmlElement for \"" + identifier + "\".");
}
}
wasPublic = isPublic;
element = e;
}
}
/*
* In a few case the annotation is not on a getter method, but directly on the field.
* The main case is the "pass" field in DefaultConformanceResult.
*/
if (element == null) try {
element = impl.getDeclaredField(identifier).getAnnotation(XmlElement.class);
assertNotNull("Missing @XmlElement annotation.", element);
} catch (NoSuchFieldException e) {
fail("Missing @XmlElement annotation.");
continue; // As a metter of principle (should never reach this point).
}
}
/*
* The UML annotation is mandatory in the default implementation of the
* 'testInterfaceAnnotations()' method, but we don't require the UML to
* be non-null here since this is not the job of this test method. This
* is because subclasses may choose to override the above test method.
*/
if (uml != null) {
assertEquals("Wrong @XmlElement.name().", getExpectedXmlElementName(type, uml), element.name());
if (!method.isAnnotationPresent(Deprecated.class) && uml.version() == 0) {
assertEquals("Wrong @XmlElement.required().", uml.obligation() == Obligation.MANDATORY, element.required());
}
}
/*
* Check that the namespace is the expected one (according subclass)
* and is not redundant with the package @XmlSchema annotation.
*/
assertExpectedNamespace(element.namespace(), impl, uml);
}
}
}
/**
* Tests the annotations on wrappers returned by {@link #getWrapperFor(Class)}.
* More specifically this method tests that:
*
* <ul>
* <li>The wrapper have a getter and a setter method declared in the same class.</li>
* <li>The getter method is annotated with {@code @XmlElement} or {@code @XmlElementRef}, but not both</li>
* <li>{@code @XmlElementRef} is used only in parent classes, not in leaf classes.</li>
* <li>The name declared in {@code @XmlElement} matches the {@code @UML} identifier.</li>
* </ul>
*/
@Test
public void testWrapperAnnotations() {
for (final Class<?> type : types) {
testingClass = type.getCanonicalName();
/*
* Check the annotation on the wrapper, if there is one. If no wrapper is declared
* specifically for the current type, check if a wrapper is defined for the parent
* interface. In such case, the getElement() method is required to be annotated by
* @XmlElementRef, not @XmlElement, in order to let JAXB infer the name from the
* actual subclass.
*/
final WrapperClass wrapper;
try {
wrapper = getWrapperInHierarchy(type);
} catch (ClassNotFoundException e) {
fail(e.toString());
continue;
}
if (wrapper.type == null) {
// If the wrapper is intentionally undefined, skip it.
continue;
}
/*
* Now fetch the getter/setter methods, ensure that they are declared in the same class
* and verify that exactly one of @XmlElement or @XmlElementRef annotation is declared.
*/
testingClass = wrapper.type.getCanonicalName();
final XmlElement element;
if (type.isEnum()) {
final Field field;
try {
field = wrapper.type.getDeclaredField("value");
} catch (NoSuchFieldException e) {
fail(e.toString());
continue;
}
element = field.getAnnotation(XmlElement.class);
} else {
final Method getter, setter;
try {
getter = wrapper.type.getMethod("getElement", (Class<?>[]) null);
setter = wrapper.type.getMethod("setElement", getter.getReturnType());
} catch (NoSuchMethodException e) {
fail(e.toString());
continue;
}
assertEquals("The setter method must be declared in the same class than the " +
"getter method - not in a parent class, to avoid issues with JAXB.",
getter.getDeclaringClass(), setter.getDeclaringClass());
assertEquals("The setter parameter type shall be the same than the getter return type.",
getter.getReturnType(), TestUtilities.getSingleton(setter.getParameterTypes()));
element = getter.getAnnotation(XmlElement.class);
assertEquals("Expected @XmlElement XOR @XmlElementRef.", (element == null),
getter.isAnnotationPresent(XmlElementRef.class) ||
getter.isAnnotationPresent(XmlElementRefs.class));
}
/*
* If the annotation is @XmlElement, ensure that XmlElement.name() is equals
* to the UML identifier. Then verify that the namespace is the expected one.
*/
if (element != null) {
assertFalse("Expected @XmlElementRef.", wrapper.isInherited);
final UML uml = type.getAnnotation(UML.class);
if (uml != null) { // 'assertNotNull' is 'testInterfaceAnnotations()' job.
assertEquals("Wrong @XmlElement.", getExpectedXmlRootElementName(null, uml), element.name());
}
final String namespace = assertExpectedNamespace(element.namespace(), wrapper.type, uml);
if (!ControlledVocabulary.class.isAssignableFrom(type)) {
final String expected = getNamespace(getImplementation(type));
if (expected != null) { // 'assertNotNull' is 'testImplementationAnnotations()' job.
assertEquals("Inconsistent @XmlRootElement namespace.", expected, namespace);
}
}
}
}
}
/**
* Prepends the {@link #testingClass} and {@link #testingMethod} before the given message.
* This is used by {@code assertFoo(…)} methods in case of failure.
*/
private String location(String message) {
if (testingClass != null) {
final StringBuilder buffer = new StringBuilder(100).append("Error with ").append(testingClass);
if (testingMethod != null) {
buffer.append('.').append(testingMethod).append("()");
}
message = buffer.append(": ").append(message).toString();
}
return message;
}
/**
* Unconditionally fails the test. This method is equivalent to JUnit {@link org.junit.Assert#fail(String)}
* except that the error message contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the failure message.
*
* @see #testingClass
* @see #testingMethod
*/
protected final void fail(final String message) {
throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given condition is false. This method is equivalent to JUnit
* {@link org.junit.Assert#assertTrue(String, boolean)} except that the error message
* contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param condition the condition that must be {@code true}.
*/
protected final void assertTrue(final String message, final boolean condition) {
if (!condition) throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given condition is true. This method is equivalent to JUnit
* {@link org.junit.Assert#assertFalse(String, boolean)} except that the error message
* contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param condition the condition that must be {@code false}.
*/
protected final void assertFalse(final String message, final boolean condition) {
if (condition) throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given object is null. This method is equivalent to JUnit
* {@link org.junit.Assert#assertNotNull(String, Object)} except that the error
* message contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param obj the object that must be non-null.
*/
protected final void assertNotNull(final String message, final Object obj) {
if (obj == null) throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given objects are the same. This method is equivalent to JUnit
* {@link org.junit.Assert#assertNotSame(String, Object, Object)} except that the error
* message contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param o1 the first object (may be null).
* @param o2 the second object (may be null).
*/
protected final void assertNotSame(final String message, final Object o1, final Object o2) {
if (o1 == o2) throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given objects are not the same. This method is equivalent to JUnit
* {@link org.junit.Assert#assertSame(String, Object, Object)} except that the error message
* contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param expected the first object (may be null).
* @param actual the second object (may be null).
*/
protected final void assertSame(final String message, final Object expected, final Object actual) {
if (expected != actual) throw new AssertionFailedError(location(message));
}
/**
* Fails the test if the given objects are not equal. This method is equivalent to JUnit
* {@link org.junit.Assert#assertEquals(String, Object, Object)} except that the error
* message contains the {@link #testingClass} and {@link #testingMethod}.
*
* @param message the message in case of failure.
* @param expected the first object (may be null).
* @param actual the second object (may be null).
*/
protected final void assertEquals(final String message, final Object expected, final Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionFailedError(location(message) + System.lineSeparator()
+ "Expected " + expected + " but got " + actual);
}
}
}