You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2018/06/26 14:47:48 UTC

[sis] branch geoapi-4.0 updated (ccb3baf -> 02c51ef)

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git.


    from ccb3baf  Replace AbstractMetadata.hashCode() and AbstractMetadata.prune() implementation by a MetadataVisitor. The intent is to share more common implementation, in particular the non-obvious parts about cycles and the use of Semaphores. For now we use MetadataVisitor only for hashCode(), isEmpty() and prune(), but we should migrate more functionalities in the future.
     new c3cc025  Add a '.gitattributes' with some instruction about whether files are text or binary.
     new 02c51ef  Replace PropertyAccessor.freeze(...) by the use of MetadataVisitor.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .gitattributes                                     |  22 ++++
 .../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 +-
 13 files changed, 322 insertions(+), 158 deletions(-)
 create mode 100644 .gitattributes
 create mode 100644 core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataVisitorException.java


[sis] 01/02: Add a '.gitattributes' with some instruction about whether files are text or binary.

Posted by de...@apache.org.
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 c3cc025030c3a1a1137dd4f304d86349ae0377c2
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon Jun 25 19:04:15 2018 +0200

    Add a '.gitattributes' with some instruction about whether files are text or binary.
---
 .gitattributes | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b9bbabf
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Convert line endings to native line endings on checkout.
+*.c          text
+*.css        text
+*.fxml       text
+*.h          text
+*.html       text
+*.idl        text
+*.java       text
+*.jsp        text
+*.prj        text
+*.properties text
+*.sh         text eol=lf
+*.txt        text
+*.xml        text
+
+# Denote files that are truly binary and should not be modified.
+*.dbf binary
+*.jpg binary
+*.png binary
+*.shp binary
+*.shx binary
+*.so  binary


[sis] 02/02: Replace PropertyAccessor.freeze(...) by the use of MetadataVisitor.

Posted by de...@apache.org.
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 <ma...@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.