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