sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/02: Replace PropertyAccessor.freeze(...) by the use of MetadataVisitor.
Date Tue, 26 Jun 2018 14:47:50 GMT
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 <martin.desruisseaux@geomatys.com>
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.


Mime
View raw message