blob: 5f48dee09674b557230b306fb4305429ed11e706 [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.Comparator;
import java.util.Map;
import java.util.HashMap;
import java.lang.reflect.Method;
import jakarta.xml.bind.annotation.XmlType;
import org.opengis.annotation.UML;
import org.opengis.annotation.Obligation;
// Specific to the main branch:
import org.opengis.metadata.citation.ResponsibleParty;
/**
* The comparator for sorting the properties in a metadata object.
* Since the comparator uses (among other criteria) the property names, this class
* incidentally defines static methods for inferring those names from the methods.
*
* <p>This comparator uses the following criteria, in priority order:</p>
* <ol>
* <li>Deprecated properties are last.</li>
* <li>If the property order is specified by a {@link XmlType} annotation,
* then this comparator complies to that order.</li>
* <li>Otherwise this comparator sorts mandatory methods first, followed by
* conditional methods, then optional ones.</li>
* <li>If the order cannot be inferred from the above, then the comparator
* fallbacks on alphabetical order.</li>
* </ol>
*
* @author Martin Desruisseaux (Geomatys)
*/
final class PropertyComparator implements Comparator<Method> {
/**
* The prefix for getters on boolean values.
*/
private static final String IS = "is";
/**
* The prefix for getters (general case).
*/
private static final String GET = "get";
/**
* The prefix for setters.
*/
static final String SET = "set";
/**
* Methods and property names specified in the {@link XmlType} annotation.
* Entries description:
*
* <ul>
* <li>Keys in this map are either {@link String} or {@link Method} instances:
* <ul>
* <li>{@code String} keys property names as given by {@link XmlType#propOrder()}.
* They are computed at construction time and do not change after construction.</li>
* <li>{@code Method} keys will be added after construction, only as needed.</li>
* </ul>
* </li>
*
* <li>Key is associated to an index that specify its position in descending order.
* For example, the property associated to integer 0 shall be sorted last.
* This descending order is only an implementation convenience.</li>
* </ul>
*/
private final Map<Object,Integer> order;
/**
* The implementation class, or the interface is the implementation class is unknown.
*/
private final Class<?> implementation;
/**
* Creates a new comparator for the given implementation class.
*
* @param implementation the implementation class, or the interface if the implementation class is unknown.
* @param standardImpl the implementation specified by the {@link MetadataStandard}, or {@code null} if none.
* This is the same as {@code implementation} unless a custom implementation is used.
*/
PropertyComparator(Class<?> implementation, final Class<?> standardImpl) {
order = new HashMap<>();
defineOrder(implementation, order);
if (order.isEmpty() && standardImpl != null && !standardImpl.isAssignableFrom(implementation)) {
/*
* We enter in this block only if the user specified its own metadata implementation and that
* custom implementation does not have any JAXB @XmlType annotation. In such case this method
* cannot sort the properties. So we will use the class defined by org.apache.sis.metadata.iso
* instead.
*/
implementation = standardImpl;
defineOrder(implementation, order);
}
this.implementation = implementation;
}
/**
* Uses the {@link XmlType} annotation for defining the property order.
*
* @param implementation the implementation class where to search for {@code XmlType} annotation.
* @param order the {@link #order} map where to store the properties order.
*/
private static void defineOrder(Class<?> implementation, final Map<Object,Integer> order) {
do {
final XmlType xml = implementation.getAnnotation(XmlType.class);
if (xml != null) {
final String[] propOrder = xml.propOrder();
for (int i=propOrder.length; --i>=0;) {
/*
* Add the entries in reverse order because we are iterating from the child class to
* the parent class, and we want the properties in the parent class to be sorted first.
* If duplicated properties are found, keep the first occurence (i.e. sort the property
* with the most specialized child that declared it).
*
* We make an exception for ResponsibleParty.role, which should be replaced by Party.role
* but this replacement is not yet effective in GeoAPI 3.0.
*/
final String prop = propOrder[i];
if (!"role".equals(prop) || !ResponsibleParty.class.isAssignableFrom(implementation)) {
order.putIfAbsent(prop, order.size());
}
}
}
implementation = implementation.getSuperclass();
} while (implementation != null);
}
/**
* Returns {@code true} if the given method is deprecated, either in the interface that declare the method
* or in the implementation class. A method may be deprecated in the implementation but not in the interface
* when the implementation has been updated for a new standard, while the interface is still reflecting the
* old standard.
*
* @param implementation the implementation class, or the interface is the implementation class is unknown.
* @param method the method to check for deprecation.
* @return {@code true} if the method is deprecated.
*/
static boolean isDeprecated(final Class<?> implementation, Method method) {
if (!MetadataStandard.IMPLEMENTATION_CAN_ALTER_API) {
return method.isAnnotationPresent(Deprecated.class);
}
if (method.isAnnotationPresent(Deprecated.class)) {
return true;
}
if (method.getDeclaringClass() == implementation) {
return false;
}
try {
method = implementation.getMethod(method.getName(), (Class[]) null);
} catch (NoSuchMethodException e) {
/*
* Should never happen since the implementation is supposed to implement
* the interface that declare the method given in argument.
*/
throw new AssertionError(e);
}
return method.isAnnotationPresent(Deprecated.class);
}
/**
* Compares the given methods for order.
*/
@Override
public int compare(final Method m1, final Method m2) {
final boolean deprecated = isDeprecated(implementation, m1);
if (deprecated != isDeprecated(implementation, m2)) {
return deprecated ? +1 : -1;
}
int c = indexOf(m2) - indexOf(m1); // indexOf(…) are sorted in descending order.
if (c == 0) {
final UML a1 = m1.getAnnotation(UML.class);
final UML a2 = m2.getAnnotation(UML.class);
if (a1 != null) {
if (a2 == null) return +1; // Sort annotated elements first.
c = order(a1) - order(a2); // Mandatory elements must be first.
if (c == 0) {
// Fallback on alphabetical order.
c = a1.identifier().compareToIgnoreCase(a2.identifier());
}
return c;
} else if (a2 != null) {
return -1; // Sort annotated elements first.
}
c = m1.getName().compareToIgnoreCase(m2.getName()); // Fallback on alphabetical order.
}
return c;
}
/**
* Returns a higher number for obligation which should be first.
*/
private static int order(final UML uml) {
final Obligation obligation = uml.obligation();
if (obligation != null) {
switch (obligation) {
case MANDATORY: return 1;
case CONDITIONAL: return 2;
case OPTIONAL: return 3;
case FORBIDDEN: return 4;
}
}
return 5;
}
/**
* Returns the index of the given method, or -1 if the method is not found.
* If positive, the index returned by this method correspond to a sorting in descending order.
*/
private int indexOf(final Method method) {
/*
* Check the cached value computed by previous call to `indexOf(…)`.
* Example: "getExtents"
*/
Integer index = order.get(method);
if (index == null) {
/*
* Check the value computed from @XmlType.propOrder() value.
* Inferred from the method name, so name is often plural.
* Example: "extents"
*/
String name = method.getName();
name = toPropertyName(name, prefix(name).length());
index = order.get(name);
if (index == null) {
/*
* Do not happen, except when we have private methods or deprecated public methods
* used as bridge between legacy and more recent standards (e.g. ISO 19115:2003 to
* ISO 19115:2014), especially when multiplicity changed between the two standards.
* Example: "extent"
*/
final UML uml = method.getAnnotation(UML.class);
if (uml == null || (index = order.get(uml.identifier())) == null) {
index = -1;
}
}
order.put(method, index);
}
return index;
}
/**
* Returns the prefix of the specified method name. If the method name doesn't starts with
* a prefix (for example {@link org.opengis.metadata.quality.ConformanceResult#pass()}),
* then this method returns an empty string.
*/
static String prefix(final String name) {
if (name.startsWith(GET)) {
return GET;
}
if (name.startsWith(IS)) {
return IS;
}
if (name.startsWith(SET)) {
return SET;
}
return "";
}
/**
* Returns {@code true} if the specified string starting at the specified index contains
* no lower case characters. The characters don't have to be in upper case however (e.g.
* non-alphabetic characters)
*/
private static boolean isAcronym(final String name, int offset) {
final int length = name.length();
while (offset < length) {
final int c = name.codePointAt(offset);
if (Character.isLowerCase(c)) {
return false;
}
offset += Character.charCount(c);
}
return true;
}
/**
* Removes the {@code "get"} or {@code "is"} prefix and turn the first character after the
* prefix into lower case. For example, the method name {@code "getTitle"} will be replaced
* by the property name {@code "title"}. We will perform this operation only if there is
* at least 1 character after the prefix.
*
* @param name the method name (cannot be {@code null}).
* @param base must be the result of {@code prefix(name).length()}.
* @return the property name (never {@code null}).
*/
static String toPropertyName(String name, final int base) {
final int length = name.length();
if (length > base) {
if (isAcronym(name, base)) {
name = name.substring(base);
} else {
final int up = name.codePointAt(base);
final int lo = Character.toLowerCase(up);
if (up != lo) {
name = new StringBuilder(length - base).appendCodePoint(lo)
.append(name, base + Character.charCount(up), length).toString();
} else {
name = name.substring(base);
}
}
}
return name.intern();
}
}