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 2023/07/02 20:10:02 UTC

[sis] branch geoapi-4.0 updated (5e334c44a4 -> 0d157264f6)

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 5e334c44a4 Remove usage of `ResourceBundle.Control` because it is not supported in the context of named modules. Move calls to `ResourceBundle.getBundle(…)` in the modules that provide the resources because `getBundle(…)` become caller-sensitive in JPMS context.
     new 3808f13930 Add a `CloneAccess` internal interface as a workaround for accessing the `clone()` method in internal classes of other modules.
     new 0d157264f6 Remove `FinalFieldSetter` (an helper class for clone and deserialization) because the reflection methods used by that class are caller-sensitive. They do not work anymore when invoked from a different module.

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:
 .../sis/coverage/grid/GridCoverageProcessor.java   |   9 +-
 .../org/apache/sis/feature/CharacteristicMap.java  |   5 +-
 .../java/org/apache/sis/feature/DenseFeature.java  |   5 +-
 .../apache/sis/feature/MultiValuedAttribute.java   |   5 +-
 .../org/apache/sis/feature/SingletonAttribute.java |   5 +-
 .../java/org/apache/sis/feature/SparseFeature.java |   5 +-
 .../sis/internal/jaxb/SpecializedIdentifier.java   |   5 +-
 .../org/apache/sis/metadata/MetadataStandard.java  |  25 +---
 .../sis/metadata/StandardImplementation.java       |   6 +-
 .../iso/quality/DefaultEvaluationMethod.java       |   3 +-
 .../sis/internal/map/coverage/RenderingData.java   |   3 +-
 .../sis/internal/referencing/AnnotatedMatrix.java  |   5 +-
 .../sis/internal/referencing/j2d/AffineMatrix.java |   3 +-
 .../org/apache/sis/parameter/TensorParameters.java |  14 +-
 .../org/apache/sis/parameter/TensorValues.java     |   3 +-
 .../org/apache/sis/internal/util/CloneAccess.java  |  48 +++++++
 .../java/org/apache/sis/internal/util/Cloner.java  | 155 +++++++++++----------
 .../apache/sis/internal/util/FinalFieldSetter.java | 128 -----------------
 .../java/org/apache/sis/measure/FormatField.java   |   4 +-
 .../org/apache/sis/measure/QuantityFormat.java     |  21 ++-
 .../java/org/apache/sis/measure/RangeFormat.java   |  19 ++-
 .../java/org/apache/sis/measure/UnitFormat.java    |  24 ++--
 .../sis/util/collection/DefaultTreeTable.java      |   4 +-
 23 files changed, 229 insertions(+), 275 deletions(-)
 create mode 100644 core/sis-utility/src/main/java/org/apache/sis/internal/util/CloneAccess.java
 delete mode 100644 core/sis-utility/src/main/java/org/apache/sis/internal/util/FinalFieldSetter.java


[sis] 02/02: Remove `FinalFieldSetter` (an helper class for clone and deserialization) because the reflection methods used by that class are caller-sensitive. They do not work anymore when invoked from a different module.

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 0d157264f6ee3b5042184ef06870f2e7005d2d20
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Jun 18 17:37:26 2023 +0200

    Remove `FinalFieldSetter` (an helper class for clone and deserialization)
    because the reflection methods used by that class are caller-sensitive.
    They do not work anymore when invoked from a different module.
---
 .../sis/coverage/grid/GridCoverageProcessor.java   |   9 +-
 .../org/apache/sis/metadata/MetadataStandard.java  |  25 +---
 .../sis/metadata/StandardImplementation.java       |   6 +-
 .../org/apache/sis/parameter/TensorParameters.java |  14 +--
 .../apache/sis/internal/util/FinalFieldSetter.java | 128 ---------------------
 .../java/org/apache/sis/measure/FormatField.java   |   4 +-
 .../org/apache/sis/measure/QuantityFormat.java     |  21 +++-
 .../java/org/apache/sis/measure/RangeFormat.java   |  19 ++-
 .../java/org/apache/sis/measure/UnitFormat.java    |  24 ++--
 9 files changed, 67 insertions(+), 183 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
index 897c4598c5..01d0d93d92 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverageProcessor.java
@@ -21,6 +21,8 @@ import java.util.Set;
 import java.util.EnumSet;
 import java.util.Objects;
 import java.util.function.Function;
+import java.lang.reflect.Field;
+import java.lang.reflect.InaccessibleObjectException;
 import java.awt.Shape;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
@@ -44,7 +46,6 @@ import org.apache.sis.internal.coverage.MultiSourceArgument;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.collection.WeakHashSet;
-import org.apache.sis.internal.util.FinalFieldSetter;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.measure.NumberRange;
 
@@ -902,12 +903,14 @@ public class GridCoverageProcessor implements Cloneable {
     public GridCoverageProcessor clone() {
         try {
             final GridCoverageProcessor clone = (GridCoverageProcessor) super.clone();
-            FinalFieldSetter.set(GridCoverageProcessor.class, "imageProcessor", clone, imageProcessor.clone());
+            final Field f = GridCoverageProcessor.class.getDeclaredField("imageProcessor");
+            f.setAccessible(true);      // Caller sensitive: must be invoked in same module.
+            f.set(clone, imageProcessor.clone());
             return clone;
         } catch (CloneNotSupportedException e) {
             throw new AssertionError(e);
         } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.cloneFailure(e);
+            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
         }
     }
 }
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 b66610bf6f..55b96066aa 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
@@ -26,7 +26,6 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.io.IOException;
 import java.io.Serializable;
 import java.io.ObjectInputStream;
-import java.io.InvalidClassException;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.ExtendedElementInformation;
@@ -39,7 +38,6 @@ import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.Semaphores;
 import org.apache.sis.internal.system.SystemListener;
 import org.apache.sis.internal.simple.SimpleCitation;
-import org.apache.sis.internal.util.FinalFieldSetter;
 import org.apache.sis.internal.util.Strings;
 
 import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
@@ -91,7 +89,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
  * by a large amount of {@link ModifiableMetadata}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  *
  * @see AbstractMetadata
  *
@@ -203,8 +201,11 @@ public class MetadataStandard implements Serializable {
      *   <li>{@link Class} if we found the interface for the type but did not yet created the {@link PropertyAccessor}.</li>
      *   <li>{@link PropertyAccessor} otherwise.</li>
      * </ul>
+     *
+     * Consider this field as final.
+     * It is not final only for {@link #readObject(ObjectInputStream)} purpose.
      */
-    private final transient ConcurrentMap<CacheKey,Object> accessors;      // Written by reflection on deserialization.
+    private transient ConcurrentMap<CacheKey,Object> accessors;
 
     /**
      * Creates a new instance working on implementation of interfaces defined in the specified package.
@@ -1093,20 +1094,6 @@ public class MetadataStandard implements Serializable {
         return Strings.bracket(getClass(), citation.getTitle());
     }
 
-    /**
-     * Assigns a {@link ConcurrentMap} instance to the given field.
-     * Used on deserialization only.
-     */
-    static <T extends MetadataStandard> void setMapForField(final Class<T> classe, final T instance, final String name)
-            throws InvalidClassException
-    {
-        try {
-            FinalFieldSetter.set(classe, name, instance, new ConcurrentHashMap<>());
-        } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.readFailure(e);
-        }
-    }
-
     /**
      * Invoked during deserialization for restoring the transient fields.
      *
@@ -1116,6 +1103,6 @@ public class MetadataStandard implements Serializable {
      */
     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
         in.defaultReadObject();
-        setMapForField(MetadataStandard.class, this, "accessors");
+        accessors = new ConcurrentHashMap<>();
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java
index 76d0f246aa..14196dcfee 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.IdentityHashMap;
 import java.util.logging.Logger;
 import java.io.ObjectStreamException;
+import java.util.concurrent.ConcurrentHashMap;
 import org.opengis.annotation.UML;
 import org.opengis.annotation.Classifier;
 import org.opengis.annotation.Stereotype;
@@ -64,6 +65,7 @@ final class StandardImplementation extends MetadataStandard {
 
     /**
      * Implementations for a given interface, computed when first needed then cached.
+     * Consider this field as final. It is not final only for {@link #readResolve()} purpose.
      *
      * <h4>Implementation note</h4>
      * In the particular case of {@code Class} keys, {@code IdentityHashMap} and {@code HashMap} have identical
@@ -72,7 +74,7 @@ final class StandardImplementation extends MetadataStandard {
      * But maybe the most interesting property is that it allocates less objects since {@code IdentityHashMap}
      * implementation doesn't need the chain of objects created by {@code HashMap}.
      */
-    private final transient Map<Class<?>,Class<?>> implementations;     // written by reflection on deserialization.
+    private transient Map<Class<?>,Class<?>> implementations;
 
     /**
      * Creates a new instance working on implementation of interfaces defined in the specified package.
@@ -185,7 +187,7 @@ final class StandardImplementation extends MetadataStandard {
          * newer version of the Apache SIS library. The newer version could contain constants
          * not yet declared in this older SIS version, so we have to use this instance.
          */
-        setMapForField(StandardImplementation.class, this, "implementations");
+        implementations = new ConcurrentHashMap<>();
         return this;
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
index 8c90808d59..893e4e66d1 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorParameters.java
@@ -39,7 +39,6 @@ import org.apache.sis.referencing.IdentifiedObjects;
 import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.internal.referencing.provider.Affine;
 import org.apache.sis.internal.referencing.Resources;
-import org.apache.sis.internal.util.FinalFieldSetter;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.measure.NumberRange;
@@ -137,7 +136,7 @@ import org.apache.sis.util.resources.Errors;
  *     }
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.2
+ * @version 1.4
  *
  * @param <E>  the type of tensor element values.
  *
@@ -284,8 +283,11 @@ public class TensorParameters<E> implements Serializable {
     /**
      * The cached descriptors for each elements in a tensor. Descriptors do not depend on tensor element values.
      * Consequently, the same descriptors can be reused for all {@link TensorValues} instances.
+     *
+     * <p>Consider this field as final.
+     * It is not final only for {@link #readObject(ObjectInputStream)} implementation.</p>
      */
-    private final transient ParameterDescriptor<E>[] parameters;
+    private transient ParameterDescriptor<E>[] parameters;
 
     /**
      * The elements for the 0 and 1 values, or {@code null} if unknown.
@@ -833,10 +835,6 @@ public class TensorParameters<E> implements Serializable {
      */
     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
         in.defaultReadObject();
-        try {
-            FinalFieldSetter.set(TensorParameters.class, "parameters", this, createCache());
-        } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.readFailure(e);
-        }
+        parameters = createCache();
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/FinalFieldSetter.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/FinalFieldSetter.java
deleted file mode 100644
index e94f42cba9..0000000000
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/FinalFieldSetter.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.internal.util;
-
-import java.lang.reflect.Field;
-import java.io.InvalidClassException;
-import java.lang.reflect.InaccessibleObjectException;
-import org.apache.sis.internal.system.Modules;
-
-
-/**
- * Convenience methods for setting the final field of an object.
- * This class shall be used only after deserialization or cloning of Apache SIS objects.
- * The usage pattern is:
- *
- * <p><b>On deserialization:</b></p>
- * {@snippet lang="java" :
- *     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
- *         in.defaultReadObject();
- *         Object someValue = ...;
- *         try {
- *             FinalFieldSetter.set(MyClass.class, "myField", this, someValue);
- *         } catch (ReflectiveOperationException e) {
- *             throw FinalFieldSetter.readFailure(e);
- *         }
- *     }
- * }
- *
- * <p><b>On clone:</b></p>
- * Same as above but invoking {@code cloneFailure(e)} if the operation failed.
- * The exception to be thrown is not the same.
- *
- * <h2>Historical note</h2>
- * Previous version was implementing {@code PrivilegedAction<FinalFieldSetter<T>>}
- * for working in the context of a security manager. This feature has been removed
- * since {@code java.security.AccessController} has been deprecated in Java 17.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
- *
- * @see <a href="https://openjdk.java.net/jeps/411">JEP-411</a>
- * @see <a href="https://issues.apache.org/jira/browse/SIS-525">SIS-525</a>
- *
- * @since 1.0
- */
-public final class FinalFieldSetter {
-    /**
-     * Do not allow instantiation of this class.
-     */
-    private FinalFieldSetter() {
-    }
-
-    /**
-     * Sets the value of the final field.
-     *
-     * @param  <T>       the type of object in which to set a final field. Should be Apache SIS classes only.
-     * @param  classe    the Apache SIS class of object for which to set a final field.
-     * @param  field     the name of the final field for which to set a value.
-     * @param  instance  the instance on which to set the value.
-     * @param  value     the value to set.
-     * @throws NoSuchFieldException if the given field has not been found.
-     * @throws IllegalAccessException if the value cannot be set.
-     */
-    public static <T> void set(final Class<T> classe, final String field, final T instance,
-            final Object value) throws NoSuchFieldException, IllegalAccessException
-    {
-        assert classe.getName().startsWith(Modules.CLASSNAME_PREFIX) : classe;
-        final Field f = classe.getDeclaredField(field);
-        f.setAccessible(true);
-        f.set(instance, value);
-    }
-
-    /**
-     * Sets the values of the final fields.
-     *
-     * @param  <T>       the type of object in which to set a final field. Should be Apache SIS classes only.
-     * @param  classe    the Apache SIS class of object for which to set a final field.
-     * @param  field     the name of the first final field for which to set a value.
-     * @param  second    the name of the second final field for which to set a value.
-     * @param  instance  the instance on which to set the value.
-     * @param  value     the value of the first field to set.
-     * @param  more      the value of the second field to set.
-     * @throws NoSuchFieldException if a given field has not been found.
-     * @throws IllegalAccessException if a value cannot be set.
-     */
-    public static <T> void set(final Class<T> classe, final String field, final String second, final T instance,
-            final Object value, final Object more) throws NoSuchFieldException, IllegalAccessException
-    {
-        set(classe, field, instance, value);
-        final Field f = classe.getDeclaredField(second);
-        f.setAccessible(true);
-        f.set(instance, more);
-    }
-
-    /**
-     * Creates an exception for a {@code readObject(ObjectInputStream)} method.
-     *
-     * @param  cause  the failure.
-     * @return the exception to throw.
-     */
-    public static InvalidClassException readFailure(final ReflectiveOperationException cause) {
-        return (InvalidClassException) new InvalidClassException(cause.getLocalizedMessage()).initCause(cause);
-    }
-
-    /**
-     * Creates an exception for a {@code clone()} method.
-     *
-     * @param  cause  the failure.
-     * @return the exception to throw.
-     */
-    public static RuntimeException cloneFailure(final ReflectiveOperationException cause) {
-        return (InaccessibleObjectException) new InaccessibleObjectException().initCause(cause);
-    }
-}
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/FormatField.java b/core/sis-utility/src/main/java/org/apache/sis/measure/FormatField.java
index 81be32cb34..a5e31d71a9 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/FormatField.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/FormatField.java
@@ -24,7 +24,7 @@ import java.text.Format;
  * Base class of format fields.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.3
+ * @version 1.4
  * @since   0.3
  */
 class FormatField extends Format.Field {
@@ -68,7 +68,7 @@ class FormatField extends Format.Field {
         final Class<?> type = getClass();
         try {
             return type.cast(type.getField(getName()).get(null));
-        } catch (Exception cause) {                                 // Many exceptions, including unchecked ones.
+        } catch (ReflectiveOperationException | ClassCastException cause) {
             throw (InvalidObjectException) new InvalidObjectException(cause.toString()).initCause(cause);
         }
     }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
index 9823f9464c..1bc913a303 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java
@@ -22,13 +22,13 @@ import java.text.FieldPosition;
 import java.text.NumberFormat;
 import java.text.ParsePosition;
 import java.io.IOException;
+import java.lang.reflect.InaccessibleObjectException;
 import javax.measure.Quantity;
 import javax.measure.Unit;
 import javax.measure.format.MeasurementParseException;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.internal.util.FinalFieldSetter;
 
 
 /**
@@ -238,13 +238,22 @@ public class QuantityFormat extends Format implements javax.measure.format.Quant
      */
     @Override
     public QuantityFormat clone() {
-        final QuantityFormat clone = (QuantityFormat) super.clone();
+        final QuantityFormat f = (QuantityFormat) super.clone();
         try {
-            FinalFieldSetter.set(QuantityFormat.class, "numberFormat", "unitFormat",
-                                 clone, numberFormat.clone(), unitFormat.clone());
+            f.clone("numberFormat");
+            f.clone("unitFormat");
         } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.cloneFailure(e);
+            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
         }
-        return clone;
+        return f;
+    }
+
+    /**
+     * Clones the value in the specified field.
+     */
+    private void clone(final String field) throws ReflectiveOperationException {
+        final var f = QuantityFormat.class.getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(this, ((Format) f.get(this)).clone());
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
index 809fc5c9ed..233402c612 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/RangeFormat.java
@@ -36,6 +36,7 @@ import java.time.format.DateTimeFormatterBuilder;
 import java.time.temporal.Temporal;
 import java.time.Instant;
 import javax.measure.Unit;
+import java.lang.reflect.InaccessibleObjectException;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.Localized;
 import org.apache.sis.util.ArgumentChecks;
@@ -43,7 +44,6 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.internal.util.LocalizedParseException;
 import org.apache.sis.internal.util.StandardDateFormat;
-import org.apache.sis.internal.util.FinalFieldSetter;
 import org.apache.sis.internal.util.Numerics;
 
 
@@ -98,7 +98,7 @@ import org.apache.sis.internal.util.Numerics;
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @see Range#toString()
  * @see <a href="https://en.wikipedia.org/wiki/ISO_31-11">Wikipedia: ISO 31-11</a>
@@ -1077,11 +1077,20 @@ public class RangeFormat extends Format implements Localized {
     public RangeFormat clone() {
         final RangeFormat f = (RangeFormat) super.clone();
         try {
-            FinalFieldSetter.set(RangeFormat.class, "elementFormat", "unitFormat",
-                                 f, elementFormat.clone(), unitFormat.clone());
+            f.clone("elementFormat");
+            f.clone("unitFormat");
         } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.cloneFailure(e);
+            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
         }
         return f;
     }
+
+    /**
+     * Clones the value in the specified field.
+     */
+    private void clone(final String field) throws ReflectiveOperationException {
+        final var f = RangeFormat.class.getDeclaredField(field);
+        f.setAccessible(true);
+        f.set(this, ((Format) f.get(this)).clone());
+    }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
index a7ea2ff712..f5a4337175 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
@@ -30,13 +30,13 @@ import java.util.ResourceBundle;
 import java.util.MissingResourceException;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.lang.reflect.InaccessibleObjectException;
 import javax.measure.Dimension;
 import javax.measure.Unit;
 import javax.measure.format.MeasurementParseException;
 import org.apache.sis.internal.system.Configuration;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.DefinitionURI;
-import org.apache.sis.internal.util.FinalFieldSetter;
 import org.apache.sis.math.Fraction;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.math.MathFunctions;
@@ -71,7 +71,7 @@ import org.apache.sis.util.logging.Logging;
  * each thread should have its own {@code UnitFormat} instance.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  *
  * @see Units#valueOf(String)
  *
@@ -1625,23 +1625,27 @@ search:     while ((i = CharSequences.skipTrailingWhitespaces(symbols, start, i)
     public UnitFormat clone() {
         final UnitFormat f = (UnitFormat) super.clone();
         try {
-            FinalFieldSetter.set(UnitFormat.class, "unitToLabel", "labelToUnit",
-                                 f, clone(unitToLabel), clone(labelToUnit));
+            f.clone("unitToLabel");
+            f.clone("labelToUnit");
         } catch (ReflectiveOperationException e) {
-            throw FinalFieldSetter.cloneFailure(e);
+            throw (InaccessibleObjectException) new InaccessibleObjectException().initCause(e);
         }
         return f;
     }
 
     /**
-     * Clones the given map, which can be either a {@link HashMap}
-     * or the instance returned by {@link Map#of()}.
+     * Clones the map in the specified field.
+     * The map can be either a {@link HashMap} or the instance returned by {@link Map#of()}.
      */
-    private static Object clone(final Map<?,?> value) {
+    private void clone(final String field) throws ReflectiveOperationException {
+        final var f = UnitFormat.class.getDeclaredField(field);
+        f.setAccessible(true);
+        Object value = f.get(this);
         if (value instanceof HashMap<?,?>) {
-            return ((HashMap<?,?>) value).clone();
+            value = ((HashMap<?,?>) value).clone();
         } else {
-            return new HashMap<>();
+            value = new HashMap<>();
         }
+        f.set(this, value);
     }
 }


[sis] 01/02: Add a `CloneAccess` internal interface as a workaround for accessing the `clone()` method in internal classes of other modules.

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 3808f1393025f5467fd8ed6a018edbaf3e0e38a4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jun 17 17:23:45 2023 +0200

    Add a `CloneAccess` internal interface as a workaround for accessing the `clone()` method in internal classes of other modules.
---
 .../org/apache/sis/feature/CharacteristicMap.java  |   5 +-
 .../java/org/apache/sis/feature/DenseFeature.java  |   5 +-
 .../apache/sis/feature/MultiValuedAttribute.java   |   5 +-
 .../org/apache/sis/feature/SingletonAttribute.java |   5 +-
 .../java/org/apache/sis/feature/SparseFeature.java |   5 +-
 .../sis/internal/jaxb/SpecializedIdentifier.java   |   5 +-
 .../iso/quality/DefaultEvaluationMethod.java       |   3 +-
 .../sis/internal/map/coverage/RenderingData.java   |   3 +-
 .../sis/internal/referencing/AnnotatedMatrix.java  |   5 +-
 .../sis/internal/referencing/j2d/AffineMatrix.java |   3 +-
 .../org/apache/sis/parameter/TensorValues.java     |   3 +-
 .../org/apache/sis/internal/util/CloneAccess.java  |  48 +++++++
 .../java/org/apache/sis/internal/util/Cloner.java  | 155 +++++++++++----------
 .../sis/util/collection/DefaultTreeTable.java      |   4 +-
 14 files changed, 162 insertions(+), 92 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/CharacteristicMap.java b/core/sis-feature/src/main/java/org/apache/sis/feature/CharacteristicMap.java
index ecca81273a..c5b19ed0ee 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/CharacteristicMap.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/CharacteristicMap.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import org.opengis.util.GenericName;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.util.Cloner;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.util.AbstractMap;
 import org.apache.sis.internal.util.AbstractMapEntry;
 import org.apache.sis.internal.feature.Resources;
@@ -36,10 +37,10 @@ import org.opengis.feature.PropertyNotFoundException;
  * This map holds only the attribute characteristics which have been explicitly set or requested.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.4
  * @since   0.5
  */
-final class CharacteristicMap extends AbstractMap<String,Attribute<?>> implements Cloneable {
+final class CharacteristicMap extends AbstractMap<String,Attribute<?>> implements CloneAccess {
     /**
      * The attribute source for which to provide characteristics.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
index 54193162b0..83815273c2 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.Arrays;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.quality.DataQuality;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.util.Cloner;
 import org.apache.sis.util.ArgumentChecks;
 
@@ -37,14 +38,14 @@ import org.opengis.feature.PropertyNotFoundException;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Marc le Bihan
- * @version 1.1
+ * @version 1.4
  *
  * @see SparseFeature
  * @see DefaultFeatureType
  *
  * @since 0.5
  */
-final class DenseFeature extends AbstractFeature implements Cloneable {
+final class DenseFeature extends AbstractFeature implements CloneAccess {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/MultiValuedAttribute.java b/core/sis-feature/src/main/java/org/apache/sis/feature/MultiValuedAttribute.java
index 1f1b7a2b28..c89fda682b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/MultiValuedAttribute.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/MultiValuedAttribute.java
@@ -17,6 +17,7 @@
 package org.apache.sis.feature;
 
 import java.util.Collection;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.util.CheckedArrayList;
 import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.ArgumentChecks;
@@ -47,7 +48,7 @@ import org.opengis.feature.MultiValuedPropertyException;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.4
  *
  * @param <V>  the type of the attribute values.
  *
@@ -55,7 +56,7 @@ import org.opengis.feature.MultiValuedPropertyException;
  *
  * @since 0.5
  */
-final class MultiValuedAttribute<V> extends AbstractAttribute<V> implements Cloneable {
+final class MultiValuedAttribute<V> extends AbstractAttribute<V> implements CloneAccess {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/SingletonAttribute.java b/core/sis-feature/src/main/java/org/apache/sis/feature/SingletonAttribute.java
index ab25bc7501..0750738d1c 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/SingletonAttribute.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/SingletonAttribute.java
@@ -17,6 +17,7 @@
 package org.apache.sis.feature;
 
 import java.util.Objects;
+import org.apache.sis.internal.util.CloneAccess;
 
 // Branch-dependent imports
 import org.opengis.feature.AttributeType;
@@ -38,7 +39,7 @@ import org.opengis.feature.AttributeType;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.4
  *
  * @param <V>  the type of the attribute value.
  *
@@ -47,7 +48,7 @@ import org.opengis.feature.AttributeType;
  * @since 0.5
  */
 @SuppressWarnings("CloneableImplementsClone")       // Nothing to add compared to subclass.
-final class SingletonAttribute<V> extends AbstractAttribute<V> implements Cloneable {
+final class SingletonAttribute<V> extends AbstractAttribute<V> implements CloneAccess {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
index 7b3ddce242..781bd7475a 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java
@@ -22,6 +22,7 @@ import java.util.Objects;
 import java.util.ConcurrentModificationException;
 import org.opengis.metadata.maintenance.ScopeCode;
 import org.opengis.metadata.quality.DataQuality;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.util.Cloner;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CorruptedObjectException;
@@ -41,14 +42,14 @@ import org.opengis.feature.PropertyNotFoundException;
  * @author  Travis L. Pinney
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.4
  *
  * @see DenseFeature
  * @see DefaultFeatureType
  *
  * @since 0.5
  */
-final class SparseFeature extends AbstractFeature implements Cloneable {
+final class SparseFeature extends AbstractFeature implements CloneAccess {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
index 4620c3c516..b783a3188f 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/SpecializedIdentifier.java
@@ -28,6 +28,7 @@ import org.apache.sis.xml.IdentifierMap;
 import org.apache.sis.xml.IdentifierSpace;
 import org.apache.sis.xml.ValueConverter;
 import org.apache.sis.util.resources.Messages;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.metadata.iso.citation.Citations;
 
 // Branch-dependent imports
@@ -40,13 +41,13 @@ import org.opengis.metadata.Identifier;
  * is an object of a type constrained by the authority.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  *
  * @param <T>  the value type, typically {@link XLink}, {@link UUID} or {@link String}.
  *
  * @since 0.3
  */
-public final class SpecializedIdentifier<T> implements Identifier, Cloneable, Serializable {
+public final class SpecializedIdentifier<T> implements Identifier, CloneAccess, Serializable {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
index a88ac7b622..98b8c69e3c 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/DefaultEvaluationMethod.java
@@ -32,6 +32,7 @@ import org.opengis.metadata.quality.EvaluationMethodType;
 import org.opengis.metadata.quality.DataEvaluation;
 import org.opengis.metadata.quality.AggregationDerivation;
 import org.apache.sis.internal.system.Semaphores;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
@@ -108,7 +109,7 @@ public class DefaultEvaluationMethod extends ISOMetadata implements EvaluationMe
      * The start and end times as a list of O, 1 or 2 elements.
      */
     private static final class Dates extends AbstractList<Temporal>
-            implements CheckedContainer<Temporal>, Cloneable, Serializable
+            implements CheckedContainer<Temporal>, CloneAccess, Serializable
     {
         /**
          * For cross-version compatibility.
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
index 0b5f9efe5b..e1d83fbd21 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java
@@ -57,6 +57,7 @@ import org.apache.sis.internal.coverage.j2d.ColorModelType;
 import org.apache.sis.internal.coverage.j2d.ImageUtilities;
 import org.apache.sis.internal.referencing.WraparoundApplicator;
 import org.apache.sis.internal.system.Modules;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.io.TableAppender;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.measure.Quantities;
@@ -106,7 +107,7 @@ import org.apache.sis.portrayal.PlanarCanvas;       // For javadoc.
  * @version 1.4
  * @since   1.1
  */
-public class RenderingData implements Cloneable {
+public class RenderingData implements CloneAccess {
     /**
      * The logger for portrayal.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
index 33ac141b41..3a53ae65a0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/AnnotatedMatrix.java
@@ -18,6 +18,7 @@ package org.apache.sis.internal.referencing;
 
 import org.opengis.referencing.operation.Matrix;
 import org.opengis.metadata.quality.PositionalAccuracy;
+import org.apache.sis.internal.util.CloneAccess;
 
 
 /**
@@ -25,10 +26,10 @@ import org.opengis.metadata.quality.PositionalAccuracy;
  * We use this class for passing additional information in methods that returns only a {@link Matrix}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   1.1
  */
-public final class AnnotatedMatrix implements Matrix, Cloneable {
+public final class AnnotatedMatrix implements Matrix, CloneAccess {
     /**
      * The matrix which contains the actual values.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
index 2d719617bd..58aab57c37 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
@@ -20,6 +20,7 @@ import java.util.Arrays;
 import java.io.Serializable;
 import java.awt.geom.AffineTransform;
 import org.opengis.referencing.operation.Matrix;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.referencing.ExtendedPrecisionMatrix;
 import org.apache.sis.referencing.operation.matrix.Matrices;
@@ -36,7 +37,7 @@ import org.apache.sis.util.ArgumentChecks;
  * @version 1.4
  * @since   0.5
  */
-class AffineMatrix extends MatrixSIS implements Serializable, Cloneable {
+class AffineMatrix extends MatrixSIS implements Serializable, CloneAccess {
     /**
      * For cross-version compatibility.
      */
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorValues.java b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorValues.java
index befec92301..501eca0d68 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorValues.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/parameter/TensorValues.java
@@ -36,6 +36,7 @@ import org.apache.sis.referencing.operation.matrix.Matrices;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.referencing.WKTUtilities;
 import org.apache.sis.internal.referencing.WKTKeywords;
+import org.apache.sis.internal.util.CloneAccess;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.util.UnmodifiableArrayList;
 import org.apache.sis.io.wkt.Formatter;
@@ -61,7 +62,7 @@ import org.apache.sis.util.resources.Errors;
  */
 @XmlTransient
 final class TensorValues<E> extends AbstractParameterDescriptor
-        implements ParameterDescriptorGroup, ParameterValueGroup, Cloneable
+        implements ParameterDescriptorGroup, ParameterValueGroup, CloneAccess
 {
     /**
      * Serial number for inter-operability with different versions.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/CloneAccess.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CloneAccess.java
new file mode 100644
index 0000000000..5b54ae7479
--- /dev/null
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CloneAccess.java
@@ -0,0 +1,48 @@
+/*
+ * 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.internal.util;
+
+
+/**
+ * Workaround for the absence of public {@code clone()} method in the standard interface.
+ * The purpose of this interface is to avoid the following exception when {@link Cloner}
+ * tries to clone a class defined in an internal package of another module:
+ *
+ * <blockquote>{@link IllegalAccessException}: class {@link Cloner}
+ * (in module {@code org.apache.sis.util}) cannot access class <var>Foo</var>
+ * (in module <var>bar</var>) because module <var>bar</var> does not export
+ * <var>foo</var> to module {@code org.apache.sis.util}</blockquote>
+ *
+ * This workaround is needed only for Apache SIS internal classes, because {@link Cloner}
+ * usage of reflection should work for exported packages.
+ *
+ * <p>This interface may be removed in any future Apache SIS version if we find a better
+ * way to workaround the lack of public {@code clone()} method in {@link Cloneable}.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.4
+ * @since   1.4
+ */
+public interface CloneAccess extends java.lang.Cloneable {
+    /**
+     * Returns a clone of this object.
+     *
+     * @return a clone of this object.
+     * @throws CloneNotSupportedException if clones are not supported.
+     */
+    Object clone() throws CloneNotSupportedException;
+}
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 29955d6da9..ad15639b7f 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
@@ -82,7 +82,7 @@ public final class Cloner {
     }
 
     /**
-     * Clones the given array, then clone all array elements recursively.
+     * Clones the given array, then clones all array elements recursively.
      *
      * @param  array          the array to clone.
      * @param  componentType  value of {@code array.getClass().getComponentType()}.
@@ -107,8 +107,8 @@ public final class Cloner {
     }
 
     /**
-     * Clones the given object. If the given object does not provide a public {@code clone()}
-     * method, then there is a choice:
+     * Clones the given object.
+     * If the given object does not provide a public {@code clone()} method, then there is a choice:
      *
      * <ul>
      *   <li>If {@code isCloneRequired(object)} returns {@code true} (the default),
@@ -128,52 +128,56 @@ public final class Cloner {
         if (result != null) {
             return result;
         }
-        final Class<?> valueType = object.getClass();
-        final Class<?> componentType = valueType.getComponentType();
-        if (componentType != null) {
-            return cloneArray(object, componentType);
-        }
-        RuntimeException security = null;
-        result = object;
-        try {
-            if (valueType != type) {
-                method = valueType.getMethod("clone", (Class<?>[]) null);
-                type = valueType;                                           // Set only if the above line succeed.
+        if (object instanceof CloneAccess) {
+            result = ((CloneAccess) object).clone();
+        } else {
+            final Class<?> valueType = object.getClass();
+            final Class<?> componentType = valueType.getComponentType();
+            if (componentType != null) {
+                return cloneArray(object, componentType);
+            }
+            RuntimeException security = null;
+            result = object;
+            try {
+                if (valueType != type) {
+                    method = valueType.getMethod("clone", (Class<?>[]) null);
+                    type = valueType;                                           // Set only if the above line succeed.
+                    /*
+                     * If the class implementing the `clone()` method is not public, we may not be able to access that
+                     * method even if it is public. Try to make the method accessible. If we fail, try to clone anyway
+                     * because maybe a parent class is accessible, but we remember the exception in order to report it
+                     * in case of failure.
+                     */
+                    if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) try {
+                        method.setAccessible(true);
+                    } catch (SecurityException | InaccessibleObjectException e) {
+                        security = e;
+                    }
+                }
                 /*
-                 * If the class implementing the `clone()` method is not public, we may not be able to access that
-                 * method even if it is public. Try to make the method accessible. If we fail for security reason,
-                 * we will still attempt to clone (maybe a parent class is public), but we remember the exception
-                 * in order to report it in case of failure.
+                 * `method` may be null if a previous call to this clone(Object) method threw NoSuchMethodException
+                 * (see the first `catch` block below). In this context, `null` means "no public clone() method".
                  */
-                if (!Modifier.isPublic(method.getDeclaringClass().getModifiers())) try {
-                    method.setAccessible(true);
-                } catch (SecurityException | InaccessibleObjectException e) {
-                    security = e;
+                if (method != null) {
+                    result = method.invoke(object, (Object[]) null);
+                }
+            } catch (NoSuchMethodException e) {
+                if (isCloneRequired) {
+                    throw fail(e, valueType);
+                }
+                method = null;
+                type = valueType;
+            } catch (IllegalAccessException e) {
+                if (security != null) {
+                    e.addSuppressed(security);
                 }
-            }
-            /*
-             * `method` may be null if a previous call to this clone(Object) method threw NoSuchMethodException
-             * (see the first `catch` block below). In this context, `null` means "no public clone() method".
-             */
-            if (method != null) {
-                result = method.invoke(object, (Object[]) null);
-            }
-        } catch (NoSuchMethodException e) {
-            if (isCloneRequired) {
+                throw fail(e, valueType);
+            } catch (InvocationTargetException e) {
+                rethrow(e.getCause());
+                throw fail(e, valueType);
+            } catch (SecurityException e) {
                 throw fail(e, valueType);
             }
-            method = null;
-            type = valueType;
-        } catch (IllegalAccessException e) {
-            if (security != null) {
-                e.addSuppressed(security);
-            }
-            throw fail(e, valueType);
-        } catch (InvocationTargetException e) {
-            rethrow(e.getCause());
-            throw fail(e, valueType);
-        } catch (SecurityException e) {
-            throw fail(e, valueType);
         }
         if (cloneResults.put(object, result) != null) {
             // Should never happen unless we have a bug.
@@ -219,6 +223,8 @@ public final class Cloner {
      * This method may be convenient when there is only one object to clone, otherwise instantiating a new
      * {@code Cloner} object is more efficient.
      *
+     * <p>Callers should test {@code if (object instanceof Cloneable)} before to invoke this method.</p>
+     *
      * @param  object  the object to clone, or {@code null}.
      * @return the given object (which may be {@code null}) or a clone of the given object.
      * @throws CloneNotSupportedException if the call to {@link Object#clone()} failed.
@@ -227,36 +233,39 @@ public final class Cloner {
      */
     @SuppressWarnings("SuspiciousSystemArraycopy")
     public static Object cloneIfPublic(final Object object) throws CloneNotSupportedException {
-        if (object != null) {
-            final Class<?> type = object.getClass();
-            final Class<?> componentType = type.getComponentType();
-            if (componentType != null) {
-                if (componentType.isPrimitive()) {
-                    final int length = Array.getLength(object);
-                    final Object copy = Array.newInstance(componentType, length);
-                    System.arraycopy(object, 0, copy, 0, length);
-                    return copy;
-                }
-                return new Cloner().cloneArray(object, componentType);
-            }
-            try {
-                final Method m = type.getMethod("clone", (Class[]) null);
-                if (Modifier.isPublic(m.getModifiers())) {
-                    return m.invoke(object, (Object[]) null);
-                }
-            } catch (NoSuchMethodException | IllegalAccessException e) {
-                /*
-                 * Should never happen because all objects have a clone() method
-                 * and we verified that the method is public.
-                 */
-                throw new AssertionError(e);
-            } catch (InvocationTargetException e) {
-                rethrow(e.getCause());
-                throw fail(e, type);
-            } catch (SecurityException e) {
-                throw fail(e, type);
+        if (object instanceof CloneAccess) {
+            return ((CloneAccess) object).clone();
+        }
+        final Class<?> type = object.getClass();
+        final Class<?> componentType = type.getComponentType();
+        if (componentType != null) {
+            if (componentType.isPrimitive()) {
+                final int length = Array.getLength(object);
+                final Object copy = Array.newInstance(componentType, length);
+                System.arraycopy(object, 0, copy, 0, length);
+                return copy;
             }
+            return new Cloner().cloneArray(object, componentType);
+        }
+        try {
+            final Method method = type.getMethod("clone", (Class[]) null);
+            return method.invoke(object, (Object[]) null);
+        } catch (NoSuchMethodException e) {
+            /*
+             * May happen if the `clone()` method is not public.
+             * The method inherited from `Object` is protected,
+             * and `getMethod(…)` does not return protected methods.
+             */
+            return object;
+        } catch (SecurityException | IllegalAccessException e) {
+            /*
+             * May happen if the class is defined in a module which
+             * does not export the package containing the class.
+             */
+            throw fail(e, type);
+        } catch (InvocationTargetException e) {
+            rethrow(e.getCause());
+            throw fail(e, type);
         }
-        return object;
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java b/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
index f46178efd1..71654783e6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
@@ -258,7 +258,9 @@ public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
     @Override
     public DefaultTreeTable clone() throws CloneNotSupportedException {
         final DefaultTreeTable clone = (DefaultTreeTable) super.clone();
-        clone.root = (TreeTable.Node) Cloner.cloneIfPublic(clone.root);
+        if (clone.root instanceof Cloneable) {
+            clone.root = (TreeTable.Node) Cloner.cloneIfPublic(clone.root);
+        }
         return clone;
     }