| /* |
| * 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.Map; |
| import java.util.List; |
| import java.util.Arrays; |
| import java.util.Locale; |
| import java.util.Iterator; |
| import java.util.Collection; |
| import java.lang.reflect.Method; |
| import java.lang.reflect.InvocationTargetException; |
| import org.opengis.annotation.UML; |
| import org.opengis.annotation.Obligation; |
| import org.opengis.metadata.ExtendedElementInformation; |
| import org.opengis.metadata.citation.Citation; |
| import org.apache.sis.util.Classes; |
| import org.apache.sis.util.Numbers; |
| import org.apache.sis.util.ArraysExt; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.Workaround; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.ObjectConverter; |
| import org.apache.sis.util.ObjectConverters; |
| import org.apache.sis.util.UnconvertibleObjectException; |
| import org.apache.sis.util.privy.CollectionsExt; |
| import org.apache.sis.util.privy.Numerics; |
| import org.apache.sis.util.privy.Unsafe; |
| import org.apache.sis.util.collection.CheckedContainer; |
| import org.apache.sis.util.collection.BackingStoreException; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.xml.IdentifiedObject; |
| import org.apache.sis.measure.ValueRange; |
| import org.apache.sis.pending.jdk.JDK19; |
| import static org.apache.sis.metadata.PropertyComparator.*; |
| import static org.apache.sis.metadata.ValueExistencePolicy.isNullOrEmpty; |
| import static org.apache.sis.util.privy.CollectionsExt.snapshot; |
| import static org.apache.sis.util.privy.CollectionsExt.modifiableCopy; |
| |
| |
| /** |
| * The getter methods declared in a GeoAPI interface, together with setter methods (if any) |
| * declared in the SIS implementation. An instance of {@code PropertyAccessor} gives access |
| * to all public properties of an instance of a metadata object. It uses reflection for this |
| * purpose, a little bit like the <cite>Java Beans</cite> framework. |
| * |
| * <p>This accessor groups the properties in two categories:</p> |
| * |
| * <ul> |
| * <li>The standard properties defined by the GeoAPI (or other standard) interfaces. |
| * Those properties are the only ones accessible by most methods in this class, except |
| * {@link #equals(Object, Object, ComparisonMode)} and {@link #walkWritable(MetadataVisitor, Object, Object)}.</li> |
| * |
| * <li>Extra properties defined by the {@link IdentifiedObject} interface. Those properties |
| * invisible in the ISO 19115-1 model, but appears in ISO 19115-3 XML marshalling. So we |
| * do the same in the SIS implementation: invisible in map and tree view, but visible in |
| * XML marshalling.</li> |
| * </ul> |
| * |
| * <h2>Thread safety</h2> |
| * The same {@code PropertyAccessor} instance can be safely used by many threads without synchronization |
| * on the part of the caller. Subclasses shall make sure that any overridden methods remain safe to call |
| * from multiple threads, because the same {@code PropertyAccessor} instances are typically used by many |
| * {@link ModifiableMetadata} instances. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| */ |
| class PropertyAccessor { |
| /** |
| * Enumeration constants for the {@code mode} argument in the |
| * {@link #count(Object, ValueExistencePolicy, int)} method. |
| */ |
| static final int COUNT_FIRST=0, COUNT_SHALLOW=1, COUNT_DEEP=2; |
| |
| /** |
| * Enumeration constants for the {@code mode} argument |
| * in the {@link #set(int, Object, Object, int)} method. |
| */ |
| static final int RETURN_NULL=0, RETURN_PREVIOUS=1, APPEND=2, IGNORE_READ_ONLY=3; |
| |
| /** |
| * Additional getter to declare in every list of getter methods that do not already provide |
| * their own {@code getIdentifiers()} method. We handle this method specially because it is |
| * needed for XML marshalling in ISO 19115-3 compliant document, while not part of abstract |
| * ISO 19115-1 specification. |
| * |
| * @see IdentifiedObject#getIdentifiers() |
| */ |
| private static final Method EXTRA_GETTER; |
| static { |
| try { |
| EXTRA_GETTER = IdentifiedObject.class.getMethod("getIdentifiers", (Class<?>[]) null); |
| } catch (NoSuchMethodException e) { |
| throw new AssertionError(e); // Should never happen. |
| } |
| } |
| |
| /** |
| * The implemented metadata interface. |
| */ |
| final Class<?> type; |
| |
| /** |
| * The implementation class, or {@link #type} if none. |
| * The following condition must hold: |
| * |
| * {@snippet lang="java" : |
| * assert type.isAssignableFrom(implementation); |
| * } |
| * |
| * <h4>Design note</h4> |
| * We could enforce the above-cited restriction with type parameter: if the {@link #type} field is declared |
| * as {@code Class<T>}, then this {@code implementation} field would be declared as {@code Class<? extends T>}. |
| * However, this is not useful for this internal class because the {@code <T>} type is never known; we have the |
| * {@code <?>} type everywhere except in tests, which result in compiler warnings at {@code PropertyAccessor} |
| * construction. |
| */ |
| final Class<?> implementation; |
| |
| /** |
| * Number of {@link #getters} methods that can be used, regardless of whether the methods are visible |
| * or hidden to the user. This is either {@code getters.length} or {@code getters.length-1}, depending |
| * on whether the {@link #EXTRA_GETTER} method needs to be skipped or not. |
| */ |
| private final int allCount; |
| |
| /** |
| * Numbers of methods to show to the user. This is always equal or lower than {@link #allCount}. |
| * This count may be lower than {@code allCount} for two reasons: |
| * |
| * <ul> |
| * <li>The {@link #EXTRA_GETTER} method is not part of the international standard.</li> |
| * <li>The interface contains deprecated methods from an older international standard. |
| * Example: changes caused by the upgrade from ISO 19115:2003 to ISO 19115:2014.</li> |
| * </ul> |
| */ |
| private final int standardCount; |
| |
| /** |
| * The public getter methods. This array should not contain any null element. |
| * They are the methods defined in the interface, not the implementation class. |
| * |
| * <p>This array shall not contains any {@code null} elements.</p> |
| */ |
| private final Method[] getters; |
| |
| /** |
| * The corresponding setter methods, or {@code null} if none. This array must have |
| * the same length as {@link #getters}. For every {@code getters[i]} element, |
| * {@code setters[i]} is the corresponding setter or {@code null} if there is none. |
| */ |
| private final Method[] setters; |
| |
| /** |
| * The JavaBeans property names. They are computed at construction time, {@linkplain String#intern() interned} |
| * then cached. Those names are often the same as field names (at least in SIS implementation), so it is |
| * reasonable to intern them in order to share {@code String} instances. |
| * |
| * <p>This array shall not contains any {@code null} elements.</p> |
| * |
| * @see #name(int, KeyNamePolicy) |
| */ |
| private final String[] names; |
| |
| /** |
| * The types of elements for the corresponding getter and setter methods. If a getter |
| * method returns a collection, then this is the type of elements in that collection. |
| * Otherwise this is the type of the returned value itself. |
| * |
| * <p>Notes:</p> |
| * <ul> |
| * <li>Element type of {@link Map} collection is {@link Map.Entry}.</li> |
| * <li>Primitive types like {@code double} or {@code int} are converted to their wrapper types.</li> |
| * <li>This array may contain null values if the type of elements in a collection is unknown |
| * (i.e. the collection is not parameterized).</li> |
| * </ul> |
| */ |
| private final Class<?>[] elementTypes; |
| |
| /** |
| * Index of getter or setter for a given name. Original names are duplicated with the same name |
| * converted to lower cases according {@link Locale#ROOT} conventions, for case-insensitive searches. |
| * This map shall be considered as immutable after construction. |
| * |
| * <p>The keys in this map are both inferred from the method names and fetched from the UML |
| * annotations. Consequently, the map may contain many entries for the same value if some |
| * method names are different than the UML identifiers.</p> |
| * |
| * @see #indexOf(String, boolean) |
| */ |
| private final Map<String,Integer> mapping; |
| |
| /** |
| * The last converter used. This is remembered on the assumption that the same converter |
| * will often be reused for the same property. This optimization can reduce the cost of |
| * looking for a converter, and also reduce thread contention since it reduces the number |
| * of calls to the synchronized {@link ObjectConverters#find(Class, Class)} method. |
| * |
| * @see #convert(Object[], Class) |
| */ |
| private transient volatile ObjectConverter<?,?> lastConverter; |
| |
| /** |
| * The property information, including the name and restrictions on valid values. |
| * The array will be created when first needed. A {@code null} element means that |
| * the information at that index has not yet been computed. |
| * |
| * @see #information(Citation, int) |
| */ |
| private transient ExtendedElementInformation[] informations; |
| |
| /** |
| * Creates a new property accessor for the specified metadata implementation. |
| * |
| * @param type the interface implemented by the metadata class. |
| * @param implementation the class of the metadata implementation, or {@code type} if none. |
| * @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. |
| */ |
| @SuppressWarnings("LocalVariableHidesMemberVariable") |
| PropertyAccessor(final Class<?> type, final Class<?> implementation, final Class<?> standardImpl) { |
| assert type.isAssignableFrom(implementation) : implementation; |
| this.type = type; |
| this.implementation = implementation; |
| this.getters = getGetters(type, implementation, standardImpl); |
| int allCount = getters.length; |
| int standardCount = allCount; |
| if (allCount != 0 && getters[allCount-1] == EXTRA_GETTER) { |
| if (!EXTRA_GETTER.getDeclaringClass().isAssignableFrom(implementation)) { |
| allCount--; // The extra getter method does not exist. |
| } |
| standardCount--; |
| } |
| while (standardCount != 0) { // Skip deprecated methods. |
| if (!isDeprecated(standardCount - 1)) { |
| break; |
| } |
| standardCount--; |
| } |
| this.allCount = allCount; |
| this.standardCount = standardCount; |
| /* |
| * Compute all information derived from getters: setters, property names, value types. |
| */ |
| mapping = JDK19.newHashMap(allCount); |
| names = new String[allCount]; |
| elementTypes = new Class<?>[allCount]; |
| Method[] setters = null; |
| final Class<?>[] arguments = new Class<?>[1]; |
| for (int i=0; i<allCount; i++) { |
| /* |
| * Fetch the getter and remind its name. We do the same for |
| * the UML tag attached to the getter, if any. |
| */ |
| final Integer index = i; |
| Method getter = getters[i]; |
| String name = getter.getName(); |
| final int base = prefix(name).length(); |
| addMapping(name, index); |
| addMappingWithLowerCase(names[i] = toPropertyName(name, base), index); |
| final UML annotation = getter.getAnnotation(UML.class); |
| if (annotation != null) { |
| addMappingWithLowerCase(annotation.identifier().intern(), index); |
| } |
| /* |
| * Now try to infer the setter from the getter. We replace the "get" prefix by |
| * "set" and look for a parameter of the same type as the getter return type. |
| */ |
| Class<?> returnType = getter.getReturnType(); |
| arguments[0] = returnType; |
| if (name.length() > base) { |
| final int lo = name.codePointAt(base); |
| final int up = Character.toUpperCase(lo); |
| final int length = name.length(); |
| final StringBuilder buffer = new StringBuilder(length - base + 5).append(SET); |
| if (lo != up) { |
| buffer.appendCodePoint(up).append(name, base + Character.charCount(lo), length); |
| } else { |
| buffer.append(name, base, length); |
| } |
| name = buffer.toString(); |
| } |
| /* |
| * Note: we want PUBLIC methods only. For example, the referencing module defines |
| * setters as private methods for use by JAXB only. We don't want to allow access |
| * to those setters. |
| */ |
| Method setter = null; |
| try { |
| setter = implementation.getMethod(name, arguments); |
| } catch (NoSuchMethodException e) { |
| /* |
| * If we found no setter method expecting an argument of the same type as the |
| * argument returned by the GeoAPI method, try again with the type returned by |
| * the implementation class. It is typically the same type, but sometimes it may |
| * be a parent type. |
| * |
| * It is a necessary condition that the type returned by the getter is assignable |
| * to the type expected by the setter. This contract is required by the `FINAL` |
| * state among others. |
| */ |
| try { |
| getter = implementation.getMethod(getter.getName(), (Class<?>[]) null); |
| } catch (NoSuchMethodException error) { |
| /* |
| * Should never happen, since the implementation class |
| * implements the interface where the getter come from. |
| */ |
| throw new AssertionError(error); |
| } |
| if (returnType != (returnType = getter.getReturnType())) { |
| arguments[0] = returnType; |
| try { |
| setter = implementation.getMethod(name, arguments); |
| } catch (NoSuchMethodException ignore) { |
| /* |
| * There is no setter, which may be normal. At this stage |
| * the `setter` variable should still have the null value. |
| */ |
| } |
| } |
| } |
| if (setter != null) { |
| if (setters == null) { |
| setters = new Method[allCount]; |
| } |
| setters[i] = setter; |
| } |
| /* |
| * Get the type of elements returned by the getter. We perform this step last because |
| * the search for a setter above may have replaced the getter declared in the interface |
| * by the getter declared in the implementation with a covariant return type. Our intent |
| * is to get a type which can be accepted by the setter. |
| */ |
| Class<?> elementType = getter.getReturnType(); |
| if (Collection.class.isAssignableFrom(elementType)) { |
| elementType = Classes.boundOfParameterizedProperty(getter); |
| if (elementType == null) { |
| // Subclass has erased parameterized type. Use method declared in the interface. |
| elementType = Classes.boundOfParameterizedProperty(getters[i]); |
| } |
| } else if (Map.class.isAssignableFrom(elementType)) { |
| elementType = Map.Entry.class; |
| } |
| elementTypes[i] = Numbers.primitiveToWrapper(elementType); |
| } |
| this.setters = setters; |
| } |
| |
| /** |
| * Adds the given (name, index) pair to {@link #mapping}, making sure we don't |
| * overwrite an existing entry with different value. |
| */ |
| private void addMapping(final String name, final Integer index) { |
| if (!name.isEmpty()) { |
| final Integer old = mapping.put(name, index); |
| if (old != null && !old.equals(index)) { |
| /* |
| * Same identifier for two different properties. If one is deprecated while the |
| * other is not, then the non-deprecated identifier overwrite the deprecated one. |
| */ |
| final boolean deprecated = isDeprecated(index); |
| if (deprecated == isDeprecated(old)) { |
| throw new IllegalStateException(Errors.format(Errors.Keys.DuplicatedIdentifier_1, |
| Classes.getShortName(type) + '.' + name)); |
| } |
| if (deprecated) { |
| mapping.put(name, old); // Restore the undeprecated method. |
| } |
| } |
| } |
| } |
| |
| /** |
| * Adds the given (name, index) pair and its lower-case variant. |
| */ |
| private void addMappingWithLowerCase(final String name, final Integer index) { |
| addMapping(name, index); |
| final String lower = name.toLowerCase(Locale.ROOT); |
| if (!lower.equals(name)) { |
| addMapping(lower, index); |
| } |
| } |
| |
| /** |
| * Returns the getters. The returned array should never be modified, |
| * since it may be shared among many instances of {@code PropertyAccessor}. |
| * |
| * @param type the metadata interface. |
| * @param implementation the class of metadata implementations, or {@code type} if none. |
| * @param standardImpl the implementation specified by the {@link MetadataStandard}, or {@code null} if none. |
| * @return the getters declared in the given interface (never {@code null}). |
| */ |
| private static Method[] getGetters(final Class<?> type, final Class<?> implementation, final Class<?> standardImpl) { |
| /* |
| * Indices map is used for choosing what to do in case of name collision. |
| */ |
| Method[] getters = (MetadataStandard.IMPLEMENTATION_CAN_ALTER_API ? implementation : type).getMethods(); |
| final Map<String,Integer> indices = JDK19.newHashMap(getters.length); |
| boolean hasExtraGetter = false; |
| int count = 0; |
| for (Method candidate : getters) { |
| if (Classes.isPossibleGetter(candidate)) { |
| final String name = candidate.getName(); |
| if (name.startsWith(SET) || SpecialCases.exclude(type, name)) { |
| continue; |
| } |
| /* |
| * The candidate method should be declared in the interface. If not, then we require it to have |
| * a @UML annotation. The latter case happen when the Apache SIS implementation contains methods |
| * for a new international standard not yet reflected in the GeoAPI interfaces. |
| */ |
| if (MetadataStandard.IMPLEMENTATION_CAN_ALTER_API) { |
| if (type == implementation) { |
| if (!type.isInterface() && !candidate.isAnnotationPresent(UML.class)) { |
| continue; // @UML considered optional only for interfaces. |
| } |
| } else try { |
| candidate = type.getMethod(name, (Class[]) null); |
| } catch (NoSuchMethodException e) { |
| if (!candidate.isAnnotationPresent(UML.class)) { |
| continue; // Not a method from an interface, and no @UML in implementation. |
| } |
| } |
| } |
| /* |
| * At this point, we are ready to accept the method. Before doing so, check if the method override |
| * another method defined in a parent class with a covariant return type. The JVM considers such |
| * cases as two different methods, while from a Java developer point of view this is the same method. |
| */ |
| final Integer pi = indices.put(name, count); |
| if (pi != null) { |
| final Class<?> pt = getters[pi].getReturnType(); |
| final Class<?> ct = candidate .getReturnType(); |
| if (ct.isAssignableFrom(pt)) { |
| continue; // Previous type was more accurate. |
| } |
| if (pt.isAssignableFrom(ct)) { |
| getters[pi] = candidate; |
| continue; |
| } |
| throw new ClassCastException(Errors.format(Errors.Keys.IllegalArgumentClass_3, |
| Classes.getShortName(type) + '.' + name, ct, pt)); |
| } |
| getters[count++] = candidate; |
| if (!hasExtraGetter) { |
| hasExtraGetter = name.equals(EXTRA_GETTER.getName()); |
| } |
| } |
| } |
| /* |
| * Sort the standard methods before to add the extra methods (if any) in order to keep |
| * the extra methods last. The code checking for extra methods requires them to be last. |
| */ |
| Arrays.sort(getters, 0, count, new PropertyComparator(implementation, standardImpl)); |
| if (!hasExtraGetter) { |
| if (getters.length == count) { |
| getters = Arrays.copyOf(getters, count+1); |
| } |
| getters[count++] = EXTRA_GETTER; |
| } |
| getters = ArraysExt.resize(getters, count); |
| return getters; |
| } |
| |
| /** |
| * Returns the number of properties that can be read. |
| * This is the properties to show in map or tree, <strong>not</strong> including |
| * hidden properties like deprecated methods or {@link #EXTRA_GETTER} method. |
| * |
| * @see #count(Object, ValueExistencePolicy, int) |
| */ |
| final int count() { |
| return standardCount; |
| } |
| |
| /** |
| * Returns the index of the specified property, or -1 if none. |
| * The search is case-insensitive. |
| * |
| * @param name the name of the property to search. |
| * @param mandatory whether this method shall throw an exception or return {@code -1} |
| * if the given name is not found. |
| * @return the index of the given name, or -1 if none and {@code mandatory} is {@code false}. |
| * @throws IllegalArgumentException if the name is not found and {@code mandatory} is {@code true}. |
| */ |
| @SuppressWarnings("StringEquality") |
| final int indexOf(final String name, final boolean mandatory) { |
| Integer index = mapping.get(name); |
| if (index == null) { |
| /* |
| * Make a second try with lower cases only if the first try failed, because |
| * most of the time the key name will have exactly the expected case and using |
| * directly the given String instance allow usage of its cached hash code value. |
| */ |
| final String key = CharSequences.replace(name, " ", "").toString().toLowerCase(Locale.ROOT).strip(); |
| if (key == name || (index = mapping.get(key)) == null) { // Identity comparison is okay here. |
| if (!mandatory) { |
| return -1; |
| } |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.PropertyNotFound_2, type, name)); |
| } |
| } |
| return index; |
| } |
| |
| /** |
| * Returns whether the property at the given index is mandatory, optional or conditional. |
| * |
| * @param index the index of the property for which to get the obligation. |
| * @return the obligation at the given index, or {@code null} if none or if the index is out of bounds. |
| */ |
| final Obligation obligation(final int index) { |
| if (index >= 0 && index < names.length) { |
| final UML uml = getters[index].getAnnotation(UML.class); |
| if (uml != null) { |
| return uml.obligation(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the name of the property at the given index, or {@code null} if none. |
| * |
| * @param index the index of the property for which to get the name. |
| * @param keyPolicy the kind of name to return. |
| * @return the name of the given kind at the given index, or {@code null} if the index is out of bounds. |
| */ |
| @SuppressWarnings("fallthrough") |
| @Workaround(library="JDK", version="10") // Actually apply to String.intern() below. |
| final String name(final int index, final KeyNamePolicy keyPolicy) { |
| if (index >= 0 && index < names.length) { |
| switch (keyPolicy) { |
| case UML_IDENTIFIER: { |
| final UML uml = getters[index].getAnnotation(UML.class); |
| if (uml != null) { |
| /* |
| * Workaround here: I though that annotation strings were interned like any other constants, |
| * but it does not seem to be the case as of JDK 10. To check if a future JDK release still |
| * needs this explicit call to String.intern(), try to remove the ".intern()" part and run |
| * the NameMapTest.testStringIntern() method. |
| */ |
| return uml.identifier().intern(); |
| } |
| // Fallthrough |
| } |
| case JAVABEANS_PROPERTY: { |
| return names[index]; |
| } |
| case METHOD_NAME: { |
| return getters[index].getName(); |
| } |
| case SENTENCE: { |
| return CharSequences.camelCaseToSentence(names[index]).toString(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the type of the property at the given index. The returned type is usually |
| * a GeoAPI interface (at least in the case of SIS implementation). |
| * |
| * <p>If the given policy is {@code ELEMENT_TYPE}, then:</p> |
| * <ul> |
| * <li>Primitive types like {@code double} or {@code int} are converted to their wrapper types.</li> |
| * <li>If the property is a collection, then returns the type of collection elements.</li> |
| * </ul> |
| * |
| * @param index the index of the property. |
| * @param policy the kind of type to return. |
| * @return the type of property values, or {@code null} if unknown. |
| */ |
| Class<?> type(final int index, final TypeValuePolicy policy) { |
| if (index >= 0 && index < allCount) { |
| switch (policy) { |
| case ELEMENT_TYPE: { |
| return elementTypes[index]; |
| } |
| case PROPERTY_TYPE: { |
| return getters[index].getReturnType(); |
| } |
| case DECLARING_INTERFACE: { |
| return getters[index].getDeclaringClass(); |
| } |
| case DECLARING_CLASS: { |
| Method getter = getters[index]; |
| if (implementation != type) try { |
| getter = implementation.getMethod(getter.getName(), (Class<?>[]) null); |
| } catch (NoSuchMethodException error) { |
| /* |
| * Should never happen, since the implementation class |
| * implements the interface where the getter come from. |
| */ |
| throw new AssertionError(error); |
| } |
| return getter.getDeclaringClass(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns {@code true} if the type at the given index is {@link Collection} or {@link Map}. |
| */ |
| final boolean isCollectionOrMap(final int index) { |
| if (index >= 0 && index < allCount) { |
| final Class<?> pt = getters[index].getReturnType(); |
| return Collection.class.isAssignableFrom(pt) || Map.class.isAssignableFrom(pt); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns {@code true} if the type at the given index is {@link Map}. |
| */ |
| final boolean isMap(final int index) { |
| return (index >= 0 && index < allCount) && elementTypes[index] == Map.Entry.class; |
| } |
| |
| /** |
| * Returns {@code true} if the property at the given index 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. |
| */ |
| private boolean isDeprecated(final int index) { |
| return PropertyComparator.isDeprecated(implementation, getters[index]); |
| } |
| |
| /** |
| * Returns the information for the property at the given index. |
| * The information are created when first needed. |
| * |
| * @param standard the standard which define the {@link #type} interface. |
| * @param index the index of the property for which to get the information. |
| * @return the information for the property at the given index, or {@code null} if the index is out of bounds. |
| * |
| * @see PropertyInformation |
| */ |
| @SuppressWarnings({"unchecked","rawtypes"}) |
| final synchronized ExtendedElementInformation information(final Citation standard, final int index) { |
| @SuppressWarnings("LocalVariableHidesMemberVariable") |
| ExtendedElementInformation[] informations = this.informations; |
| if (informations == null) { |
| this.informations = informations = new PropertyInformation<?>[standardCount]; |
| } |
| if (index < 0 || index >= informations.length) { |
| return null; |
| } |
| ExtendedElementInformation information = informations[index]; |
| if (information == null) { |
| final Class<?> elementType = elementTypes[index]; |
| final String name = name(index, KeyNamePolicy.UML_IDENTIFIER); |
| final Method getter = getters[index]; |
| final ValueRange range; |
| try { |
| range = implementation.getMethod(getter.getName(), (Class<?>[]) null).getAnnotation(ValueRange.class); |
| } catch (NoSuchMethodException error) { |
| /* |
| * Should never happen, since the implementation class |
| * implements the interface where the getter come from. |
| */ |
| throw new AssertionError(error); |
| } |
| information = new PropertyInformation<>(standard, name, getter, elementType, range); |
| informations[index] = information; |
| } |
| return information; |
| } |
| |
| /** |
| * Returns a remark or warning to format with the value at the given index, or {@code null} if none. |
| * This is provided when the value may look surprising, for example the longitude values in a geographic |
| * bounding box crossing the anti-meridian. |
| */ |
| CharSequence remarks(int index, Object metadata) { |
| return null; |
| } |
| |
| /** |
| * Returns {@code true} if the {@link #implementation} class has at least one setter method. |
| */ |
| final boolean isWritable() { |
| return setters != null; |
| } |
| |
| /** |
| * Returns {@code true} if the property at the given index is writable. |
| */ |
| final boolean isWritable(final int index) { |
| return (index >= 0) && (index < allCount) && (setters != null) && (setters[index] != null); |
| } |
| |
| /** |
| * Returns the value for the specified metadata, or {@code null} if none. |
| * If the given index is out of bounds, then this method returns {@code null}, |
| * so it is safe to invoke this method even if {@link #indexOf(String, boolean)} |
| * returned -1. |
| * |
| * @param index the index of the property for which to get a value. |
| * @param metadata the metadata object to query. |
| * @return the value, or {@code null} if none or if the given is out of bounds. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| */ |
| Object get(final int index, final Object metadata) throws BackingStoreException { |
| return (index >= 0 && index < allCount) ? get(getters[index], metadata) : null; |
| } |
| |
| /** |
| * Gets a value from the specified metadata. We do not expect any checked exception to be |
| * thrown, since classes in the {@code org.opengis.metadata} packages do not declare any. |
| * However if a checked exception is throw anyway (maybe in user defined "standard"), it |
| * will be wrapped in a {@link BackingStoreException}. Unchecked exceptions are propagated. |
| * |
| * @param method the method to use for the query. |
| * @param metadata the metadata object to query. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| * |
| * @see #set(Method, Object, Object[]) |
| */ |
| private static Object get(final Method method, final Object metadata) throws BackingStoreException { |
| assert (method.getReturnType() != Void.TYPE) : method; |
| try { |
| try { |
| return method.invoke(metadata, (Object[]) null); |
| } catch (IllegalArgumentException e) { |
| /* |
| * May happen if the getter method is defined only in the implementation class — not in the interface — |
| * but the given metadata object is an instance of another implementation class than the expected one. |
| * Example: CI_Citation.graphics didn't existed in ISO 19115:2003 and has been added in ISO 19115:2014. |
| * Consequently, there is no Citation.getGraphics() method in GeoAPI 3.0 interfaces (only in GeoAPI 3.1), |
| * but there is a DefaultCitation.getGraphics() method in Apache SIS implementation since some versions |
| * are ahead of GeoAPI. But if the given `metadata` instance is a different implementation of Citation |
| * interface, then attempt to invoke DefaultCitation.getGraphics() fail with IllegalArgumentException. |
| * In such case, we check if that implementation has a public method with exactly same signature. |
| * If yes, we try to invoke that method before to give up. |
| */ |
| if (method.getDeclaringClass().isInstance(metadata)) { |
| throw e; // Exception thrown for another reason. This is probably a bug. |
| } |
| if (MetadataStandard.IMPLEMENTATION_CAN_ALTER_API) try { |
| final Method specific = metadata.getClass().getMethod(method.getName(), method.getParameterTypes()); |
| if (method.getReturnType().equals(specific.getReturnType())) { |
| return specific.invoke(metadata, (Object[]) null); |
| } |
| } catch (NoSuchMethodException ex) { |
| // Ignore. |
| } |
| return null; |
| } |
| } catch (IllegalAccessException e) { |
| /* |
| * Should never happen since `getters` should contain only public methods. |
| */ |
| throw new AssertionError(method.toString(), e); |
| } catch (InvocationTargetException e) { |
| /* |
| * Exception in user code (not a wrong usage of reflection). |
| */ |
| final Throwable cause = e.getTargetException(); |
| if (cause instanceof RuntimeException) { |
| throw (RuntimeException) cause; |
| } |
| if (cause instanceof Error) { |
| throw (Error) cause; |
| } |
| throw new BackingStoreException(cause); |
| } |
| } |
| |
| /** |
| * Sets a value for the specified metadata and returns the old value if {@code mode} is |
| * {@link #RETURN_PREVIOUS}. The {@code mode} argument can be one of the following: |
| * |
| * <ul> |
| * <li>RETURN_NULL: Set the value and returns {@code null}.</li> |
| * <li>RETURN_PREVIOUS: Set the value and returns the previous value. If the previous value was a |
| * collection or a map, then that value is copied in a new collection or map |
| * before the new value is set because the setter methods typically copy the |
| * new collection in their existing instance.</li> |
| * <li>APPEND: Set the value only if it does not overwrite an existing value, then returns |
| * {@link Boolean#TRUE} if the metadata changed as a result of this method call, |
| * {@link Boolean#FALSE} if the metadata didn't changed or {@code null} if the |
| * value cannot be set because another value already exists.</li> |
| * <li>IGNORE_READ_ONLY: Set the value and returns {@code null} on success. If the property is read-only, |
| * do not throw an exception; returns exception class instead.</li> |
| * </ul> |
| * |
| * <p>The {@code APPEND} mode has an additional side effect: it sets the {@code append} argument to |
| * {@code true} in the call to the {@link #convert(Method, Object, Object, Object[], Class, boolean)} |
| * method. See the {@code convert} javadoc for more information.</p> |
| * |
| * <p>If the given index is out of bounds, then this method does nothing and return {@code null}. |
| * We do that because the {@link ValueMap#remove(Object)} method may invoke this method with |
| * an index of -1 if the {@link #indexOf(String, boolean)} method didn't found the property name. |
| * However, the given value will be silently discarded, so index out-of-bounds shall be used only |
| * in the context of {@code remove} operations (this is not verified).</p> |
| * |
| * @param index the index of the property to set. |
| * @param metadata the metadata object on which to set the value. |
| * @param value the new value. |
| * @param mode whether this method should first fetches the old value, |
| * as one of the constants listed in this method javadoc. |
| * @return the old value, or {@code null} if {@code mode} was {@code RETURN_NULL} or {@code IGNORE_READ_ONLY}. |
| * @throws UnmodifiableMetadataException if the property for the given key is read-only. |
| * @throws ClassCastException if the given value is not of the expected type. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| */ |
| Object set(final int index, final Object metadata, final Object value, final int mode) |
| throws UnmodifiableMetadataException, ClassCastException, BackingStoreException |
| { |
| if (index < 0 || index >= allCount) { |
| return null; |
| } |
| if (setters != null) { |
| final Method getter = getters[index]; |
| final Method setter = setters[index]; |
| if (setter != null) { |
| final Object oldValue; |
| final Object snapshot; // Copy of oldValue before modification. |
| switch (mode) { |
| case IGNORE_READ_ONLY: |
| case RETURN_NULL: { |
| oldValue = null; |
| snapshot = null; |
| break; |
| } |
| case APPEND: { |
| oldValue = get(getter, metadata); |
| snapshot = null; |
| break; |
| } |
| case RETURN_PREVIOUS: { |
| oldValue = get(getter, metadata); |
| if (oldValue instanceof Collection<?>) { |
| if (oldValue instanceof List<?>) { |
| snapshot = snapshot((List<?>) oldValue); |
| } else { |
| snapshot = modifiableCopy((Collection<?>) oldValue); |
| } |
| } else if (oldValue instanceof Map<?,?>) { |
| snapshot = modifiableCopy((Map<?,?>) oldValue); |
| } else { |
| snapshot = oldValue; |
| } |
| break; |
| } |
| default: throw new AssertionError(mode); |
| } |
| /* |
| * Converts the new value to a type acceptable for the setter method (if possible). |
| * If the new value is a singleton while the expected type is a collection, then the `convert` |
| * method added the singleton in the existing collection, which may result in no change if the |
| * collection is a Set and the new value already exists in that Set. If we detect that there is |
| * no change, then we don't need to invoke the setter method. Note that we conservatively assume |
| * that there is always a change in RETURN_NULL mode since we don't know the previous value. |
| */ |
| final Object[] newValues = new Object[] {value}; |
| Boolean changed = convert(getter, metadata, oldValue, newValues, elementTypes[index], mode == APPEND); |
| if (changed == null) { |
| changed = (mode == RETURN_NULL) || (mode == IGNORE_READ_ONLY) || (newValues[0] != oldValue); |
| if (changed && mode == APPEND && !isNullOrEmpty(oldValue)) { |
| /* |
| * If `convert` did not added the value in a collection and if a value already |
| * exists, do not modify the existing value. Exit now with "no change" status. |
| */ |
| return null; |
| } |
| } |
| if (changed) { |
| set(setter, metadata, newValues); |
| } |
| return (mode == APPEND) ? changed : snapshot; |
| } |
| } |
| if (mode == IGNORE_READ_ONLY) { |
| return UnmodifiableMetadataException.class; |
| } |
| throw new UnmodifiableMetadataException(Errors.format( |
| Errors.Keys.CanNotSetPropertyValue_1, type.getSimpleName() + '.' + names[index])); |
| } |
| |
| /** |
| * Sets a value for the specified metadata. This method does not attempt any conversion of |
| * argument values. Conversion of type, if needed, must have been applied before to call |
| * this method. |
| * |
| * <p>We do not expect any checked exception to be thrown, since classes in the |
| * {@code org.opengis.metadata} packages do not declare any. However if a checked |
| * exception is throw anyway, then it will be wrapped in a {@link BackingStoreException}. |
| * Unchecked exceptions are propagated.</p> |
| * |
| * @param setter the method to use for setting the new value. |
| * @param metadata the metadata object to query. |
| * @param newValues the argument to give to the method to be invoked. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| * |
| * @see #get(Method, Object) |
| */ |
| private static void set(final Method setter, final Object metadata, final Object[] newValues) |
| throws BackingStoreException |
| { |
| try { |
| setter.invoke(metadata, newValues); |
| } catch (IllegalAccessException e) { |
| // Should never happen since `setters` should contain only public methods. |
| throw new AssertionError(e); |
| } catch (InvocationTargetException e) { |
| final Throwable cause = e.getTargetException(); |
| if (cause instanceof RuntimeException) { |
| throw (RuntimeException) cause; |
| } |
| if (cause instanceof Error) { |
| throw (Error) cause; |
| } |
| throw new BackingStoreException(cause); |
| } |
| } |
| |
| /** |
| * Converts a value to the type required by a setter method. |
| * The values are converted in-place in the {@code newValues} array. We use an array instead |
| * of a single argument and return value because an array will be needed anyway for invoking |
| * the {@link #convert(Object[], Class)} and {@link Method#invoke(Object, Object[])} methods. |
| * |
| * <h4>The collection special case</h4> |
| * If the metadata property is a collection, then there is a choice: |
| * |
| * <ul> |
| * <li>If {@code append} is {@code true}, then the new value (which may itself be a collection) |
| * is unconditionally added to the previous collection.</li> |
| * <li>If {@code append} is {@code false} and the new value is <strong>not</strong> a collection, |
| * then the new value is added to the existing collection. In other words, we behave as a |
| * <i>multi-values map</i> for the properties that allow multi-values.</li> |
| * <li>Otherwise the new collection replaces the previous collection. All previous values |
| * are discarded.</li> |
| * </ul> |
| * |
| * Adding new values to the previous collection may or may not change the original metadata |
| * depending on whether those collections are live or not. In Apache SIS implementation, |
| * those collections are live. However, this method can be though as if the collections were |
| * not live, since the caller will invoke the setter method with the collection anyway. |
| * |
| * @param getter the method to use for fetching the previous value. |
| * @param metadata the metadata object to query and modify. |
| * @param oldValue the value returned by {@code get(getter, metadata)}, or {@code null} if unknown. |
| * This parameter is only an optimization for avoiding to invoke the getter method |
| * twice if the value is already known. |
| * @param newValues the argument to convert. The content of this array will be modified in-place. |
| * Current implementation requires an array of length 1, however this restriction |
| * may be relaxed in a future SIS version if needed. |
| * @param elementType the target type (if singleton) or the type of elements in the collection. |
| * @param append if {@code true} and the value is a collection, then that collection will be added |
| * to any previously existing collection instead of replacing it. |
| * @return if the given value has been added to an existing collection, then whether that existing |
| * collection has been modified as a result of this method call. Otherwise {@code null}. |
| * @throws ClassCastException if the element of the {@code arguments} array is not of the expected type. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| */ |
| private Boolean convert(final Method getter, final Object metadata, Object oldValue, final Object[] newValues, |
| Class<?> elementType, final boolean append) throws ClassCastException, BackingStoreException |
| { |
| assert newValues.length == 1; |
| Object newValue = newValues[0]; |
| Class<?> targetType = getter.getReturnType(); |
| if (newValue == null) { |
| // Cannot test elementType, because it has been converted to the wrapper class. |
| if (targetType.isPrimitive()) { |
| newValues[0] = Numbers.valueOfNil(targetType); |
| } |
| return null; |
| } |
| Boolean changed = null; |
| if (!Collection.class.isAssignableFrom(targetType)) { |
| /* |
| * We do not expect a collection. The provided argument should not be a |
| * collection neither. It should be some class convertible to targetType. |
| * |
| * If nevertheless the user provided a collection and this collection contains |
| * no more than 1 element, then as a convenience we will extract the singleton |
| * element and process it as if it had been directly provided in argument. |
| */ |
| if (newValue instanceof Collection<?>) { |
| final Iterator<?> it = ((Collection<?>) newValue).iterator(); |
| if (!it.hasNext()) { // If empty, process like null argument. |
| newValues[0] = null; |
| return null; |
| } |
| final Object next = it.next(); |
| if (!it.hasNext()) { // Singleton |
| newValue = next; |
| } |
| /* |
| * Other cases: let the collection unchanged. It is likely to |
| * cause an exception later. The message should be appropriate. |
| */ |
| } |
| targetType = Numbers.primitiveToWrapper(targetType); |
| } else { |
| /* |
| * We expect a collection. Collections are handled in one of the two ways below: |
| * |
| * - If the user gives a collection, the user's collection replaces any |
| * previous one. The content of the previous collection is discarded. |
| * |
| * - If the user gives a single value, it will be added to the existing |
| * collection (if any). The previous values are not discarded. This |
| * allow for incremental filling of a property. |
| * |
| * The code below prepares an array of elements to be converted and wraps that |
| * array in a List (to be converted to a Set after this block if required). It |
| * is okay to convert the elements after the List creation since the list is a |
| * wrapper. |
| */ |
| final boolean isCollection = (newValue instanceof Collection<?>); |
| final Object[] elements = isCollection ? ((Collection<?>) newValue).toArray() : new Object[] {newValue}; |
| final List<Object> elementList = Arrays.asList(elements); // Converted later (see above comment). |
| newValue = elementList; // Still contains the same values, but now guaranteed to be a collection. |
| Collection<?> addTo = null; |
| if (!isCollection || append) { |
| if (oldValue == null) { |
| oldValue = get(getter, metadata); |
| } |
| if (oldValue != null) { |
| addTo = (Collection<?>) oldValue; |
| if (addTo instanceof CheckedContainer<?>) { |
| // Get the explicitly-specified element type. |
| elementType = ((CheckedContainer<?>) addTo).getElementType(); |
| } |
| newValue = addTo; |
| } |
| } |
| if (elementType != null) { |
| convert(elements, elementType); |
| } |
| /* |
| * We now have objects of the appropriate type. If we have a singleton to be added |
| * in an existing collection, add it now. In that case the `newValue` should refer |
| * to the `addTo` collection. We rely on the ModifiableMetadata.writeCollection(…) |
| * optimization for detecting that the new collection is the same instance as |
| * the old one so there is nothing to do. We could exit from the method, but let |
| * it continues in case the user override the `setFoo(…)` method. |
| */ |
| if (addTo != null) { |
| /* |
| * Unsafe addition into a collection. In SIS implementation, the collection is |
| * actually an instance of CheckedCollection, so the check will be performed at |
| * runtime. However, other implementations could use unchecked collection. |
| * There is not much we can do... |
| */ |
| changed = Unsafe.addAll(addTo, elementList); |
| } |
| } |
| /* |
| * If the expected type was not a collection, the conversion of user value happen |
| * here. Otherwise conversion from List to Set (if needed) happen here. |
| */ |
| newValues[0] = newValue; |
| convert(newValues, targetType); |
| return changed; |
| } |
| |
| /** |
| * Converts values in the specified array to the given type. |
| * The array content is modified in-place. This method accepts an array instead of |
| * a single value because the values to convert may be the content of a collection. |
| * |
| * @param elements the array which contains element to convert. |
| * @param targetType the base type of target elements. |
| * @throws ClassCastException if an element cannot be converted. |
| */ |
| @SuppressWarnings({"unchecked","rawtypes"}) |
| private void convert(final Object[] elements, final Class<?> targetType) throws ClassCastException { |
| boolean hasNewConverter = false; |
| ObjectConverter converter = null; // Intentionally raw type. Checks are done below. |
| for (int i=0; i<elements.length; i++) { |
| final Object value = elements[i]; |
| if (value != null) { |
| final Class<?> sourceType = value.getClass(); |
| if (!targetType.isAssignableFrom(sourceType)) try { |
| if (converter == null) { |
| converter = lastConverter; // Volatile field - read only if needed. |
| } |
| /* |
| * Require the exact same classes, not parent or subclass, |
| * otherwise the converter could be stricter than necessary. |
| */ |
| if (converter == null || converter.getSourceClass() != sourceType |
| || converter.getTargetClass() != targetType) |
| { |
| converter = ObjectConverters.find(sourceType, targetType); |
| hasNewConverter = true; |
| } |
| elements[i] = converter.apply(value); |
| } catch (UnconvertibleObjectException cause) { |
| throw (ClassCastException) new ClassCastException(Errors.format( |
| Errors.Keys.IllegalClass_2, targetType, sourceType)).initCause(cause); |
| } |
| } |
| } |
| if (hasNewConverter) { |
| lastConverter = converter; // Volatile field - store only if needed. |
| } |
| } |
| |
| /** |
| * Counts the number of non-null or non-empty properties. |
| * The {@code mode} argument can be one of the following: |
| * |
| * <ul> |
| * <li>COUNT_FIRST: stop at the first property found. This mode is used for testing if a |
| * metadata is empty or not, without the need to known the exact count.</li> |
| * <li>COUNT_SHALLOW: count all properties, counting collections as one property.</li> |
| * <li>COUNT_DEEP: count all properties, counting collections as the number of |
| * properties returned by {@link Collection#size()}.</li> |
| * </ul> |
| * |
| * @param mode kinds of count, as described above. |
| * @param valuePolicy the behavior of the count toward null or empty values. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| * |
| * @see #count() |
| */ |
| final int count(final Object metadata, final ValueExistencePolicy valuePolicy, final int mode) |
| throws BackingStoreException |
| { |
| assert type.isInstance(metadata) : metadata; |
| if (valuePolicy == ValueExistencePolicy.ALL && mode != COUNT_DEEP) { |
| return count(); |
| } |
| int count = 0; |
| // Use `standardCount` instead of `allCount` for ignoring deprecated methods. |
| for (int i=0; i<standardCount; i++) { |
| final Object value = get(getters[i], metadata); |
| if (!valuePolicy.isSkipped(value)) { |
| switch (mode) { |
| case COUNT_FIRST:{ |
| return 1; |
| } |
| case COUNT_SHALLOW:{ |
| count++; |
| break; |
| } |
| case COUNT_DEEP: { |
| /* |
| * Count always at least one element because if the user wanted to skip null or empty |
| * collections, then `valuePolicy.isSkipped(value)` above would have returned `true`. |
| */ |
| count += isCollectionOrMap(i) ? Math.max(CollectionsExt.size(value), 1) : 1; |
| break; |
| } |
| default: throw new AssertionError(mode); |
| } |
| } |
| } |
| return count; |
| } |
| |
| /** |
| * Compares the two specified metadata objects. This method implements a <em>shallow</em> comparison, |
| * i.e. all metadata properties are compared using their {@code properties.equals(…)} method |
| * without explicit calls to this {@code accessor.equals(…)} method for children. |
| * However, the final result may still be a deep comparison. |
| * |
| * @param metadata1 the first metadata object to compare. This object determines the accessor. |
| * @param metadata2 the second metadata object to compare. |
| * @param mode the strictness level of the comparison. |
| * @throws BackingStoreException if the implementation threw a checked exception. |
| * |
| * @see MetadataStandard#equals(Object, Object, ComparisonMode) |
| */ |
| public boolean equals(final Object metadata1, final Object metadata2, final ComparisonMode mode) |
| throws BackingStoreException |
| { |
| assert type.isInstance(metadata1) : metadata1; |
| assert type.isInstance(metadata2) : metadata2; |
| for (int i=0; i<standardCount; i++) { |
| final Method method = getters[i]; |
| final Object value1 = get(method, metadata1); |
| final Object value2 = get(method, metadata2); |
| if (isNullOrEmpty(value1) && isNullOrEmpty(value2)) { |
| /* |
| * Consider empty collections/arrays as equal to null. |
| * Empty strings are also considered equal to null (this is more questionable). |
| */ |
| continue; |
| } |
| final boolean equals; |
| if ((value1 instanceof Double || value1 instanceof Float) && |
| (value2 instanceof Double || value2 instanceof Float)) |
| { |
| equals = Numerics.epsilonEqual(((Number) value1).doubleValue(), |
| ((Number) value2).doubleValue(), mode); |
| } else { |
| equals = Utilities.deepEquals(value1, value2, mode); |
| } |
| if (!equals) { |
| assert (mode != ComparisonMode.DEBUG) : type.getSimpleName() + '.' + names[i] + " differ."; |
| return false; |
| } |
| } |
| /* |
| * One final check for the IdentifiedObjects.getIdentifiers() collection. |
| */ |
| if (mode == ComparisonMode.STRICT && EXTRA_GETTER.getDeclaringClass().isInstance(metadata2)) { |
| final Object value1 = get(EXTRA_GETTER, metadata1); |
| final Object value2 = get(EXTRA_GETTER, metadata2); |
| if (!isNullOrEmpty(value1) || !isNullOrEmpty(value2)) { |
| return Utilities.deepEquals(value1, value2, mode); |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Invokes {@link MetadataVisitor#visit(Class, Object)} for all non-null properties in the given metadata. |
| * This method is not recursive, i.e. it does not traverse the children of the elements in the given metadata. |
| * |
| * @param visitor the object on which to invoke {@link MetadataVisitor#visit(Class, Object)}. |
| * @param metadata the metadata instance for which to visit the non-null properties. |
| * @throws Exception if an error occurred while visiting a property. |
| */ |
| final void walkReadable(final MetadataVisitor<?> visitor, final Object metadata) throws Exception { |
| assert type.isInstance(metadata) : metadata; |
| for (int i=0; i<standardCount; i++) { |
| visitor.setCurrentProperty(names[i]); |
| final Object value = get(getters[i], metadata); |
| if (value != null) { |
| final Object result = visitor.visit(elementTypes[i], value); |
| if (result != value) { |
| if (result == MetadataVisitor.SKIP_SIBLINGS) break; |
| set(i, metadata, result, IGNORE_READ_ONLY); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Invokes {@link MetadataVisitor#visit(Class, Object)} for all writable properties in the given metadata. |
| * This method is not recursive, i.e. it does not traverse the children of the elements in the given metadata. |
| * |
| * <h4>Constraint</h4> |
| * In current implementation, if {@code source} and {@code target} are not the same, |
| * then {@code target} is assumed empty. The intent is to skip easily null or empty properties. |
| * |
| * @param visitor the object on which to invoke {@link MetadataVisitor#visit(Class, Object)}. |
| * @param source the metadata from which to read properties. May be the same as {@code target}. |
| * @param target the metadata instance where to write properties. |
| * @throws Exception if an error occurred while visiting a property. |
| */ |
| final void walkWritable(final MetadataVisitor<?> visitor, final Object source, final Object target) throws Exception { |
| assert type.isInstance(source) : source; |
| assert type.isInstance(target) : target; |
| if (setters == null || !implementation.isInstance(target)) { |
| return; |
| } |
| final Object[] arguments = new Object[1]; |
| for (int i=0; i<allCount; i++) { |
| visitor.setCurrentProperty(names[i]); |
| final Method setter = setters[i]; |
| if (setter != null) { |
| if (setter.isAnnotationPresent(Deprecated.class)) { |
| /* |
| * We need to skip deprecated setter methods, because those methods may delegate |
| * their work to other setter methods in different objects and those objects may |
| * have been made unmodifiable by previous iteration in this loop. If we do not |
| * skip them, we may get an UnmodifiableMetadataException in the call to set(…). |
| * |
| * Note that in some cases, only the setter method is deprecated, not the getter. |
| * This happen when Apache SIS classes represent a more recent ISO standard than |
| * the GeoAPI interfaces. |
| */ |
| continue; |
| } |
| final Object value = get(getters[i], source); |
| final Object result = visitor.visit(elementTypes[i], value); |
| if (source == target ? (result != value) : !isNullOrEmpty(result)) { // See "constraint" in Javadoc |
| if (result == MetadataVisitor.SKIP_SIBLINGS) break; |
| arguments[0] = result; |
| set(setter, target, arguments); |
| /* |
| * We invoke the set(…) method variant that do not perform type conversion |
| * because we do not want it to replace the immutable collections created |
| * by ModifiableMetadata.unmodifiable(source). Conversions should not be |
| * required anyway because the getter method should have returned a value |
| * compatible with the setter method - this contract is ensured by the |
| * way the PropertyAccessor constructor selected the setter methods. |
| */ |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns a string representation of this accessor for debugging purpose. |
| * Output example: |
| * |
| * <pre class="text">PropertyAccessor[13 getters & 13 setters in DefaultCitation:Citation]</pre> |
| */ |
| @Override |
| public String toString() { |
| final StringBuilder buffer = new StringBuilder(60); |
| buffer.append("PropertyAccessor[").append(standardCount).append(" getters"); |
| final int extra = allCount - standardCount; |
| if (extra != 0) { |
| buffer.append(" (+").append(extra).append(" ext.)"); |
| } |
| if (setters != null) { |
| int c = 0; |
| for (final Method setter : setters) { |
| if (setter != null) { |
| c++; |
| } |
| } |
| buffer.append(" & ").append(c).append(" setters"); |
| } |
| buffer.append(" in ").append(Classes.getShortName(implementation)); |
| if (type != implementation) { |
| buffer.append(':').append(Classes.getShortName(type)); |
| } |
| return buffer.append(']').toString(); |
| } |
| } |