This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 02c51ef99cbf19917d2e66aa524e924f067b9da6 Author: Martin Desruisseaux <[email protected]> AuthorDate: Tue Jun 26 16:47:16 2018 +0200 Replace PropertyAccessor.freeze(...) by the use of MetadataVisitor. --- .../main/java/org/apache/sis/metadata/Freezer.java | 80 +++++++------ .../java/org/apache/sis/metadata/HashCode.java | 8 +- .../org/apache/sis/metadata/MetadataStandard.java | 4 +- .../org/apache/sis/metadata/MetadataVisitor.java | 65 +++++++---- .../sis/metadata/MetadataVisitorException.java | 100 ++++++++++++++++ .../apache/sis/metadata/ModifiableMetadata.java | 13 +-- .../org/apache/sis/metadata/PropertyAccessor.java | 126 ++++++++++----------- .../main/java/org/apache/sis/metadata/Pruner.java | 14 ++- .../java/org/apache/sis/internal/util/Cloner.java | 32 +++--- .../java/org/apache/sis/util/resources/Errors.java | 10 ++ .../apache/sis/util/resources/Errors.properties | 2 + .../apache/sis/util/resources/Errors_fr.properties | 4 +- 12 files changed, 300 insertions(+), 158 deletions(-) diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/Freezer.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/Freezer.java index 02de4a5..a3621b2 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/Freezer.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/Freezer.java @@ -32,22 +32,21 @@ import org.apache.sis.metadata.iso.identification.DefaultRepresentativeFraction; /** * Returns unmodifiable view of metadata elements of arbitrary type. - * Despite the {@code Cloner} parent class name, this class actually - * tries to avoid creating new clones as much as possible. + * This class tries to avoid creating new clones as much as possible. * * @author Martin Desruisseaux (Geomatys) * @version 1.0 * @since 0.3 * @module */ -final class Freezer extends Cloner { +final class Freezer extends MetadataVisitor<Boolean> { /** * The {@code Freezer} instance in current use. The clean way would have been to pass the {@code Freezer} * instance in argument to all {@code freeze()} and {@code unmodifiable()} methods in metadata packages. * But above-cited methods are public, and we do not want to expose {@code Freezer} in public API for now. * This thread-local is a workaround for that situation. */ - private static final ThreadLocal<Freezer> CURRENT = ThreadLocal.withInitial(Freezer::new); + private static final ThreadLocal<Freezer> VISITORS = ThreadLocal.withInitial(Freezer::new); /** * All objects made immutable during iteration over children properties. @@ -56,9 +55,9 @@ final class Freezer extends Cloner { private final Map<Object,Object> existings; /** - * Usage count, for determining when to clean {@link #CURRENT}. + * The cloner, created when first needed. */ - private int useCount; + private Cloner cloner; /** * Creates a new {@code Freezer} instance. @@ -68,22 +67,29 @@ final class Freezer extends Cloner { } /** - * Returns the freezer in current use, or a new one if none. - * Callers <strong>must</strong> invoke {@link #release()} in a {@code finally} block. + * Returns the visitor for the current thread if it already exists, or creates a new one otherwise. */ - static Freezer acquire() { - final Freezer freezer = CURRENT.get(); - freezer.useCount++; - return freezer; + static Freezer getOrCreate() { + return VISITORS.get(); } /** - * Release this freezer after usage. + * Returns the thread-local variable that created this {@code Freezer} instance. */ - final void release() { - if (--useCount == 0) { - CURRENT.remove(); - } + @Override + final ThreadLocal<Freezer> creator() { + return VISITORS; + } + + /** + * Notifies {@link MetadataVisitor} that we want to visit all writable properties. + * + * @param type ignored. + * @return {@code true}, for iterating over all writable properties. + */ + @Override + boolean preVisit(final Class<?> type) { + return true; } /** @@ -100,20 +106,11 @@ final class Freezer extends Cloner { } /** - * Tells {@link Cloner#clone(Object)} to return the original object - * if no public {@code clone()} method is found. + * Recursively freezes all elements in the given array. */ - @Override - protected boolean isCloneRequired(final Object object) { - return false; - } - - /** - * Recursively clones all elements in the given array. - */ - private void clones(final Object[] array) throws CloneNotSupportedException { + private void freezeAll(final Object[] array) throws CloneNotSupportedException { for (int i=0; i < array.length; i++) { - array[i] = clone(array[i]); + array[i] = visit(null, array[i]); } } @@ -132,11 +129,12 @@ final class Freezer extends Cloner { * <li>Otherwise, the object is assumed immutable and returned unchanged.</li> * </ul> * + * @param type ignored (can be {@code null}). * @param object the object to convert in an immutable one. * @return a presumed immutable view of the specified object. */ @Override - public Object clone(final Object object) throws CloneNotSupportedException { + final Object visit(final Class<?> type, final Object object) throws CloneNotSupportedException { /* * CASE 1 - The object is an org.apache.sis.metadata.* implementation. It may have * its own algorithm for creating an unmodifiable view of metadata. @@ -165,7 +163,7 @@ final class Freezer extends Cloner { break; } case 1: { - final Object value = clone(array[0]); + final Object value = visit(null, array[0]); collection = isSet ? Collections.singleton(value) : Collections.singletonList(value); break; @@ -177,7 +175,7 @@ final class Freezer extends Cloner { } else if (collection instanceof CodeListSet<?>) { collection = Collections.unmodifiableSet(((CodeListSet<?>) collection).clone()); } else { - clones(array); + freezeAll(array); collection = CollectionsExt.immutableSet(false, array); } } else { @@ -186,7 +184,7 @@ final class Freezer extends Cloner { * Conservatively assumes a List if we are not sure to have a Set since the list * is less destructive (no removal of duplicated values). */ - clones(array); + freezeAll(array); collection = UnmodifiableArrayList.wrap(array); } break; @@ -201,7 +199,7 @@ final class Freezer extends Cloner { if (object instanceof Map<?,?>) { final Map<Object,Object> map = new LinkedHashMap<>((Map<?,?>) object); for (final Map.Entry<Object,Object> entry : map.entrySet()) { - entry.setValue(clone(entry.getValue())); + entry.setValue(visit(null, entry.getValue())); } return CollectionsExt.unmodifiableOrCopy(map); } @@ -209,11 +207,23 @@ final class Freezer extends Cloner { * CASE 4 - The object is presumed cloneable. */ if (object instanceof Cloneable) { - return unique(super.clone(object)); + if (cloner == null) { + cloner = new Cloner(false); + } + return unique(cloner.clone(object)); } /* * CASE 5 - Any other case. The object is assumed immutable and returned unchanged. */ return unique(object); } + + /** + * Returns an arbitrary value used by {@link MetadataVisitor} for remembering that + * a metadata instance has been processed. + */ + @Override + Boolean result() { + return Boolean.TRUE; + } } diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/HashCode.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/HashCode.java index 3b7bb3a..161e814 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/HashCode.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/HashCode.java @@ -56,7 +56,7 @@ final class HashCode extends MetadataVisitor<Integer> { * Returns the thread-local variable that created this {@code HashCode} instance. */ @Override - final ThreadLocal<? extends MetadataVisitor<?>> creator() { + final ThreadLocal<HashCode> creator() { return VISITORS; } @@ -65,10 +65,12 @@ final class HashCode extends MetadataVisitor<Integer> { * If another hash code computation was in progress, that code shall be saved before this method is invoked. * * @param type the standard interface of the metadata for which a hash code value will be computed. + * @return {@code false} since this visitor is not restricted to writable properties. */ @Override - void preVisit(final Class<?> type) { + boolean preVisit(final Class<?> type) { code = type.hashCode(); + return false; } /** @@ -90,7 +92,7 @@ final class HashCode extends MetadataVisitor<Integer> { c += value.hashCode(); code = c; } - return null; + return value; } /** diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java index 4517504..711bc2f 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java @@ -926,7 +926,9 @@ public class MetadataStandard implements Serializable { * @see ModifiableMetadata#freeze() */ final void freeze(final Object metadata) throws ClassCastException { - getAccessor(new CacheKey(metadata.getClass()), true).freeze(metadata); + if (metadata != null) { + Freezer.getOrCreate().walk(this, null, metadata, true); + } } /** diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitor.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitor.java index 4b9c414..e3e5225 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitor.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitor.java @@ -16,6 +16,7 @@ */ package org.apache.sis.metadata; +import java.util.Arrays; import java.util.Map; import java.util.IdentityHashMap; import java.util.ConcurrentModificationException; @@ -38,16 +39,6 @@ import org.apache.sis.internal.system.Semaphores; */ abstract class MetadataVisitor<R> { /** - * Sentinel value that may be returned by {@link #visit(Class, Object)} for meaning that a property value - * should be set to {@code null}. If the property type is a collection, then "null" value is interpreted - * as an instruction to {@linkplain java.util.Collection#clear() clear} the collection. - * - * <div class="note"><b>Note:</b> a sentinel value is required because {@code visit(…)} already uses - * the {@code null} return value for meaning that the property value shall not be modified.</div> - */ - static final Object CLEAR = Void.TYPE; // The choice of this type is arbitrary. - - /** * Sentinel value that may be returned by {@link #visit(Class, Object)} for notifying the walker to stop. * This value causes {@link #walk walk(…)} to stop its iteration, but does not stop iteration by the parent * if {@code walk(…)} has been invoked recursively. The {@link #result()} method shall return a valid result @@ -73,6 +64,13 @@ abstract class MetadataVisitor<R> { private final Map<Object,R> visited; /** + * The name of the property being visited as the last element of the queue. If {@code visit} method + * is invoked recursively, then the properties before the last one are the parent properties. + * The number of valid elements is {@link #nestedCount}. + */ + private String[] propertyPath; + + /** * Count of nested calls to {@link #walk(MetadataStandard, Class, Object, boolean)} method. * When this count reach zero, the visitor should be removed from the thread local variable. */ @@ -93,6 +91,7 @@ abstract class MetadataVisitor<R> { */ protected MetadataVisitor() { visited = new IdentityHashMap<>(); + propertyPath = new String[6]; } /** @@ -102,6 +101,13 @@ abstract class MetadataVisitor<R> { abstract ThreadLocal<? extends MetadataVisitor<?>> creator(); /** + * Sets the name of the method being visited. This is invoked by {@code PropertyAccessor.walk} methods only. + */ + final void setCurrentProperty(final String name) { + propertyPath[nestedCount - 1] = name; + } + + /** * Invokes {@link #visit(Class, Object)} for all elements of the given metadata if that metadata has not * already been visited. The computation result is returned (may be the result of a previous computation). * @@ -121,11 +127,14 @@ abstract class MetadataVisitor<R> { if (!visited.containsKey(metadata)) { // Reminder: the associated value may be null. final PropertyAccessor accessor = standard.getAccessor(new CacheKey(metadata.getClass(), type), mandatory); if (accessor != null) { - preVisit(accessor.type); + final boolean write = preVisit(accessor.type); if (visited.put(metadata, null) != null) { // Should never happen, unless this method is invoked concurrently in another thread. throw new ConcurrentModificationException(); } + if (nestedCount >= propertyPath.length) { + propertyPath = Arrays.copyOf(propertyPath, nestedCount * 2); + } if (nestedCount++ == 0) { /* * The NULL_COLLECTION semaphore prevents creation of new empty collections by getter methods @@ -137,7 +146,15 @@ abstract class MetadataVisitor<R> { allowNull = Semaphores.queryAndSet(Semaphores.NULL_COLLECTION); } try { - accessor.walk(this, metadata); + if (write) { + accessor.walkWritable(this, metadata); + } else { + accessor.walkReadable(this, metadata); + } + } catch (MetadataVisitorException e) { + throw e; + } catch (Exception e) { + throw new MetadataVisitorException(Arrays.copyOf(propertyPath, nestedCount), accessor.type, e); } finally { if (--nestedCount == 0) { if (!allowNull) { @@ -161,8 +178,11 @@ abstract class MetadataVisitor<R> { * {@link #visit(Class, Object)} will be invoked for each property in the metadata object. * * @param type the standard interface implemented by the metadata instance being visited. + * @return {@code true} for visiting only writable properties, or + * {@code false} for visiting all readable properties. */ - void preVisit(Class<?> type) { + boolean preVisit(Class<?> type) { + return false; } /** @@ -170,22 +190,23 @@ abstract class MetadataVisitor<R> { * The return value is interpreted as below: * * <ul> - * <li>{@link #SKIP_SIBLINGS}: do not iterate over other elements of current metadata, - * but continue iteration over elements of the parent metadata.</li> - * <li>{@link #CLEAR}: clear the property value (e.g. by setting it to {@code null}), - * then continue with next sibling property.</li> - * <li>Any other non-null value: set the property value to the given value, + * <li>{@link #SKIP_SIBLINGS}: do not iterate over other properties of current metadata, + * but continue iteration over properties of the parent metadata.</li> + * <li>{@code value}: continue with next sibling property without setting any value.</li> + * <li>{@code null}: clear the property value, then continue with next sibling property. + * If the property type is a collection, then "null" value is interpreted as an instruction + * to {@linkplain java.util.Collection#clear() clear} the collection.</li> + * <li>Any other value: set the property value to the given value, * then continue with next sibling property.</li> - * <li>{@code null}: continue with next sibling property without setting any value.</li> * </ul> * * @param type the type of elements. Note that this is not necessarily the type * of given {@code value} argument if the later is a collection. * @param value value of the metadata property being visited. - * @return one of the sentinel values ({@link #CLEAR} or {@link #SKIP_SIBLINGS}), - * or the new property value to set, or {@code null} for leaving the property value unchanged. + * @return the new property value to set, or {@link #SKIP_SIBLINGS}. + * @throws Exception if the visit operation failed. */ - abstract Object visit(Class<?> type, Object value); + abstract Object visit(Class<?> type, Object value) throws Exception; /** * Returns the result of visiting all elements in a metadata instance. diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitorException.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitorException.java new file mode 100644 index 0000000..6fa1949 --- /dev/null +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitorException.java @@ -0,0 +1,100 @@ +/* + * 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 org.apache.sis.internal.util.UnmodifiableArrayList; +import org.apache.sis.util.LocalizedException; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.resources.Errors; +import org.opengis.util.InternationalString; + + +/** + * Thrown when a {@link MetadataVisitor#visit(Class, Object)} method failed. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.0 + * @since 1.0 + * @module + */ +final class MetadataVisitorException extends BackingStoreException implements LocalizedException { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 3779183393705626697L; + + /** + * Path to the element that we failed to process. + */ + private final String[] propertyPath; + + /** + * Constructs a new exception with the specified cause. + * + * @param path path to the element that we failed to process. + * @param type the class that was visited when the exception occurred. + * @param cause the cause, saved for later retrieval by the {@link #getCause()} method. + */ + public MetadataVisitorException(final String[] path, final Class<?> type, final Exception cause) { + super(type.getSimpleName(), cause); + propertyPath = path; + } + + /** + * Returns an error message giving the location of the failure together with the cause. + */ + @Override + public String getMessage() { + return getInternationalMessage().toString(); + } + + /** + * Returns an error message giving the location of the failure together with the cause. + */ + @Override + public InternationalString getInternationalMessage() { + short key = Errors.Keys.CanNotProcessProperty_2; + int count = 2; + String location = super.getMessage(); + int pathLength = propertyPath.length; + if (pathLength != 0) { + location += '.' + propertyPath[--pathLength]; + if (pathLength != 0) { + key = Errors.Keys.CanNotProcessPropertyAtPath_3; + count = 3; + } + } + final Throwable cause = getCause(); + Object message = null; + if (cause instanceof LocalizedException) { + message = ((LocalizedException) cause).getInternationalMessage(); + } + if (message == null) { + message = cause.getLocalizedMessage(); + if (message == null) { + message = cause.getClass(); + } + } + final Object[] arguments = new Object[count]; + arguments[--count] = message; + arguments[--count] = location; + if (count != 0) { + arguments[0] = String.join(".", UnmodifiableArrayList.wrap(propertyPath, 0, pathLength)); + } + return Errors.formatInternational(key, arguments); + } +} diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java index 4bd36d5..eeaa56f 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java @@ -213,23 +213,12 @@ public abstract class ModifiableMetadata extends AbstractMetadata implements Clo public void freeze() { if (isModifiable()) { ModifiableMetadata success = null; - /* - * The NULL_COLLECTION semaphore prevents creation of new empty collections by getter methods - * (a consequence of lazy instantiation). The intent is to avoid creation of unnecessary objects - * for all unused properties. Users should not see behavioral difference, except if they override - * some getters with an implementation invoking other getters. However in such cases, users would - * have been exposed to null values at XML marshalling time anyway. - */ - final boolean allowNull = Semaphores.queryAndSet(Semaphores.NULL_COLLECTION); try { unmodifiable = FREEZING; getStandard().freeze(this); success = this; } finally { unmodifiable = success; - if (!allowNull) { - Semaphores.clear(Semaphores.NULL_COLLECTION); - } } } } @@ -248,7 +237,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata implements Clo if (unmodifiable == this) { throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata)); } else if (unmodifiable != FREEZING) { - unmodifiable = null; + unmodifiable = null; // Discard since this metadata is going to change. } } } diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java index 07da234..33ed5f8 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java @@ -63,8 +63,8 @@ import static org.apache.sis.util.collection.Containers.hashMapCapacity; * * <ul> * <li>The standard properties defined by the GeoAPI (or other standard) interfaces. - * Those properties are the only one accessible by most methods in this class, - * except {@link #equals(Object, Object, ComparisonMode)} and {@link #freeze(Object)}.</li> + * Those properties are the only ones accessible by most methods in this class, except + * {@link #equals(Object, Object, ComparisonMode)} and {@link #walkWritable(MetadataVisitor, 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 @@ -1185,61 +1185,6 @@ class PropertyAccessor { } /** - * Replaces every properties in the specified metadata by their - * {@linkplain ModifiableMetadata#unmodifiable() unmodifiable variant}. - * This method also replaces duplicated elements by single instances. - * - * @throws BackingStoreException if the implementation threw a checked exception. - */ - final void freeze(final Object metadata) throws BackingStoreException { - assert implementation.isInstance(metadata) : metadata; - if (setters == null) { - return; - } - final Object[] arguments = new Object[1]; - final Freezer freezer = Freezer.acquire(); - try { - for (int i=0; i<allCount; 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 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 Method getter = getters[i]; - final Object source = get(getter, metadata); - final Object target = freezer.clone(source); - if (source != target) { - arguments[0] = target; - set(setter, metadata, arguments); - /* - * We invoke the set(…) method variant that do not perform type conversion - * because we don't want it to replace the immutable collection created - * by ModifiableMetadata.unmodifiable(source). Conversion 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. - */ - } - } - } - } catch (CloneNotSupportedException e) { - throw new UnsupportedOperationException(e); - } finally { - freezer.release(); - } - } - - /** * Returns a potentially deep copy of the given metadata object. * * @param metadata the metadata object to copy. @@ -1280,17 +1225,68 @@ class PropertyAccessor { * * @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 walk(final MetadataVisitor<?> visitor, final Object metadata) { + final void walkReadable(final MetadataVisitor<?> visitor, final Object metadata) throws Exception { assert type.isInstance(metadata) : metadata; for (int i=0; i<standardCount; i++) { - final Object element = get(getters[i], metadata); - if (element != null) { - Object r = visitor.visit(elementTypes[i], element); - if (r != null) { - if (r == MetadataVisitor.SKIP_SIBLINGS) break; - if (r == MetadataVisitor.CLEAR) r = null; - set(i, metadata, r, IGNORE_READ_ONLY); + 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. + * + * @param visitor the object on which to invoke {@link MetadataVisitor#visit(Class, Object)}. + * @param metadata the metadata instance for which to visit the writable properties. + * @throws Exception if an error occurred while visiting a property. + */ + final void walkWritable(final MetadataVisitor<?> visitor, final Object metadata) throws Exception { + assert implementation.isInstance(metadata) : metadata; + if (setters == null) { + 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], metadata); + final Object result = visitor.visit(elementTypes[i], value); + if (result != value) { + if (result == MetadataVisitor.SKIP_SIBLINGS) break; + arguments[0] = result; + set(setter, metadata, 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. + */ } } } diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/Pruner.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/Pruner.java index d61b546..f29ed4d 100644 --- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/Pruner.java +++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/Pruner.java @@ -66,7 +66,7 @@ final class Pruner extends MetadataVisitor<Boolean> { * Returns the thread-local variable that created this {@code Pruner} instance. */ @Override - final ThreadLocal<? extends MetadataVisitor<?>> creator() { + final ThreadLocal<Pruner> creator() { return VISITORS; } @@ -90,11 +90,17 @@ final class Pruner extends MetadataVisitor<Boolean> { /** * Marks a metadata instance as empty before we start visiting its non-null properties. - * If the metadata does not contain any property, then this field will stay {@code true}. + * If the metadata does not contain any property, then the {@link #isEmpty} field will + * stay {@code true}. + * + * @return {@code false} since this visitor is not restricted to writable properties. + * We need to visit all readable properties even for pruning operation since + * we need to determine if the metadata is empty. */ @Override - void preVisit(Class<?> type) { + boolean preVisit(final Class<?> type) { isEmpty = true; + return false; } /** @@ -176,7 +182,7 @@ final class Pruner extends MetadataVisitor<Boolean> { * If all elements were empty, set the whole property to 'null'. */ isEmpty = isEmptyMetadata & isEmptyValue; - return isEmptyValue & prune ? CLEAR : null; + return isEmptyValue & prune ? null : value; } /** diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Cloner.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Cloner.java index 795c381..878d2ec 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Cloner.java +++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Cloner.java @@ -28,12 +28,12 @@ import org.apache.sis.util.resources.Errors; * for the lack of public {@code clone()} method in the {@link Cloneable} interface. * * @author Martin Desruisseaux (Geomatys) - * @version 0.6 + * @version 1.0 * @since 0.3 * @module */ @Workaround(library="JDK", version="1.7") -public class Cloner { +public final class Cloner { /** * The type of the object to clone, or {@code null} if not yet specified. * Used for checking if the cached {@linkplain #method} is still valid. @@ -47,25 +47,27 @@ public class Cloner { private Method method; /** - * Creates a new {@code Cloner} instance. + * Action to take when an object can not be cloned because no public {@code clone()} method has been found. + * If this field is {@code true}, then the {@link #clone(Object)} method in this class will throw a + * {@link CloneNotSupportedException}. Otherwise the {@code clone(Object)} method will return the original object. + */ + private final boolean isCloneRequired; + + /** + * Creates a new {@code Cloner} instance which requires public {@code clone()} method to be present. */ public Cloner() { + isCloneRequired = true; } /** - * Invoked when the given object can not be cloned because no public {@code clone()} method - * has been found. If this method returns {@code true}, then the {@link #clone(Object)} - * method in this class will throw a {@link CloneNotSupportedException}. Otherwise the - * {@code clone(Object)} method will return the original object. - * - * <p>The default implementation returns {@code true} in every cases. - * Subclasses can override this method if they need a different behavior.</p> + * Creates a new {@code Cloner} instance. * - * @param object the object that can not be cloned. - * @return {@code true} if the problem shall be considered a clone failure. + * @param isCloneRequired whether a {@link CloneNotSupportedException} should be thrown if no public + * {@code clone()} method is found. */ - protected boolean isCloneRequired(final Object object) { - return true; + public Cloner(final boolean isCloneRequired) { + this.isCloneRequired = isCloneRequired; } /** @@ -113,7 +115,7 @@ public class Cloner { return method.invoke(object, (Object[]) null); } } catch (NoSuchMethodException e) { - if (isCloneRequired(object)) { + if (isCloneRequired) { throw fail(e, valueType); } method = null; diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java index 2b15ca1..cadcaf0 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java @@ -136,6 +136,16 @@ public final class Errors extends IndexedResourceBundle { public static final short CanNotParse_1 = 180; /** + * Can not process property “{1}” located at path “{0}”. The reason is: {2} + */ + public static final short CanNotProcessPropertyAtPath_3 = 184; + + /** + * Can not process property “{0}”. The reason is: {1} + */ + public static final short CanNotProcessProperty_2 = 185; + + /** * Can not read property “{1}” in file “{0}”. */ public static final short CanNotReadPropertyInFile_2 = 11; diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties index 864ce7c..a3053bf 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties @@ -38,6 +38,8 @@ CanNotCopy_1 = Can not copy \u201c{0}\u201d. CanNotOpen_1 = Can not open \u201c{0}\u201d. CanNotParse_1 = Can not parse \u201c{0}\u201d. CanNotParseFile_2 = Can not parse \u201c{1}\u201d as a file in the {0} format. +CanNotProcessProperty_2 = Can not process property \u201c{0}\u201d. The reason is: {1} +CanNotProcessPropertyAtPath_3 = Can not process property \u201c{1}\u201d located at path \u201c{0}\u201d. The reason is: {2} CanNotRead_1 = Can not read \u201c{0}\u201d. CanNotReadPropertyInFile_2 = Can not read property \u201c{1}\u201d in file \u201c{0}\u201d. CanNotRepresentInFormat_2 = Can not represent \u201c{1}\u201d in a strictly standard-compliant {0} format. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties index 15f2d1d..bce1748 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties +++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties @@ -35,6 +35,8 @@ CanNotCopy_1 = Ne peut pas copier \u00ab\u202f{0}\u202f\u00 CanNotOpen_1 = Ne peut pas ouvrir \u00ab\u202f{0}\u202f\u00bb. CanNotParse_1 = Ne peut pas interpr\u00e9ter \u00ab\u202f{0}\u202f\u00bb. CanNotParseFile_2 = Ne peut pas lire \u00ab\u202f{1}\u202f\u00bb comme un fichier au format {0}. +CanNotProcessProperty_2 = Ne peut pas traiter la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb pour la raison suivante\u2008: {1} +CanNotProcessPropertyAtPath_3 = Ne peut pas traiter la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb d\u00e9sign\u00e9e par le chemin \u00ab\u202f{0}\u202f\u00bb pour la raison suivante\u2008: {2} CanNotRead_1 = Ne peut pas lire \u00ab\u202f{0}\u202f\u00bb. CanNotReadPropertyInFile_2 = Ne peut pas lire la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb. CanNotRepresentInFormat_2 = Ne peut pas repr\u00e9senter \u00ab\u202f{1}\u202f\u00bb dans un format {0} strictement conforme. @@ -73,7 +75,7 @@ IllegalArgumentClass_2 = L\u2019argument \u2018{0}\u2019 ne peut pas IllegalArgumentClass_3 = L\u2019argument \u2018{0}\u2019 ne peut pas \u00eatre de type \u2018{2}\u2019. Une instance de \u2018{1}\u2019 ou d\u2019un type d\u00e9riv\u00e9 \u00e9tait attendue. IllegalArgumentField_4 = L\u2019argument \u2018{0}\u2019 n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb parce que le champs \u2018{2}\u2019 ne peut pas prendre la valeur \u00ab\u202f{3}\u202f\u00bb. IllegalArgumentValue_2 = L\u2019argument \u2018{0}\u2019 n\u2019accepte pas la valeur \u00ab\u202f{1}\u202f\u00bb. -IllegalBitsPattern_1 = Pattern de bits invalide: {0}. +IllegalBitsPattern_1 = Pattern de bits invalide\u2008: {0}. IllegalClass_2 = La classe \u2018{1}\u2019 est ill\u00e9gale. Il doit s\u2019agir d\u2019une classe \u2018{0}\u2019 ou d\u00e9riv\u00e9e. IllegalCharacter_2 = Le caract\u00e8re \u00ab\u202f{1}\u202f\u00bb ne peut pas \u00eatre utilis\u00e9 dans \u00ab\u202f{0}\u202f\u00bb. IllegalCharacterForFormat_3 = Le caract\u00e8re \u00ab\u202f{2}\u202f\u00bb dans \u00ab\u202f{1}\u202f\u00bb n\u2019est pas permis par le format \u00ab\u202f{0}\u202f\u00bb.
