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.

Reply via email to