sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 03/03: Add a ModifiableMetadata.deepCopy(…) method.
Date Wed, 19 Aug 2020 10:36:19 GMT
This is an automated email from the ASF dual-hosted git repository.

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

commit e6cffdba3b1c545d0fb3fccf7b6ff3831ebe238f
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Tue Aug 18 16:05:22 2020 +0200

    Add a ModifiableMetadata.deepCopy(…) method.
---
 .../org/apache/sis/metadata/MetadataCopier.java    |  2 +-
 .../org/apache/sis/metadata/MetadataVisitor.java   | 35 ++++++++---
 .../apache/sis/metadata/ModifiableMetadata.java    | 68 +++++++++++++++++++++-
 .../java/org/apache/sis/metadata/StateChanger.java | 27 +++++----
 .../sis/metadata/ModifiableMetadataTest.java       | 42 ++++++++++++-
 5 files changed, 153 insertions(+), 21 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
index 1e34396..33cda77 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
@@ -114,7 +114,7 @@ public class MetadataCopier extends MetadataVisitor<Object> {
             @Override protected Object copyRecursively(final Class<?> type, final Object
metadata) {
                 if (metadata instanceof ModifiableMetadata) {
                     final ModifiableMetadata.State state = ((ModifiableMetadata) metadata).state();
-                    if (state == ModifiableMetadata.State.FINAL) {
+                    if (state != null && state.isUnmodifiable()) {
                         return metadata;
                     }
                 }
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 80859d2..d69b274 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
@@ -51,8 +51,8 @@ abstract class MetadataVisitor<R> {
 
     /**
      * A guard against infinite recursivity in {@link #walk(MetadataStandard, Class, Object,
boolean)}.
-     * Keys are visited metadata instances and values are computed value. The value may be
null if
-     * the computation is in progress.
+     * Keys are visited metadata instances and values are computed value.
+     * The value may be null if the computation is in progress.
      *
      * <h4>The problem</h4>
      * Cyclic associations can exist in ISO 19115 metadata. For example {@code Instrument}
has a reference
@@ -76,6 +76,8 @@ abstract class MetadataVisitor<R> {
     /**
      * 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.
+     *
+     * @see #creator()
      */
     private int nestedCount;
 
@@ -101,6 +103,9 @@ abstract class MetadataVisitor<R> {
      * The thread-local variable that created this {@code MetadataVisitor} instance.
      * This is usually a static final {@code VISITORS} constant defined in the subclass.
      * May be {@code null} if this visitor does not use thread-local instances.
+     *
+     * <p>If this method returns a non-null value, then {@link ThreadLocal#remove()}
will be invoked
+     * after {@link MetadataVisitor} finished to walk through a metadata and all its children.</p>
      */
     ThreadLocal<? extends MetadataVisitor<?>> creator() {
         return null;
@@ -133,8 +138,7 @@ abstract class MetadataVisitor<R> {
      * already been visited. The computation result is returned (may be the result of a previous
computation).
      *
      * <p>This method is typically invoked recursively while we iterate down the metadata
tree.
-     * It creates a map of visited nodes when the iteration begin, and deletes that map when
the
-     * iteration ends.</p>
+     * It maintains a map of visited nodes for preventing the same node to be visited twice.</p>
      *
      * @param  standard   the metadata standard implemented by the object to visit.
      * @param  type       the standard interface implemented by the metadata object, or {@code
null} if unknown.
@@ -148,9 +152,9 @@ 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) {
-                final Filter filter = preVisit(accessor);
-                final boolean preconstructed;
-                final R sentinel;
+                final Filter filter = preVisit(accessor);       // NONE, NON_EMPTY, WRITABLE
or WRITABLE_RESULT.
+                final boolean preconstructed;                   // Whether to write in instance
provided by `result()`.
+                final R sentinel;                               // Value in the map for telling
that visit started.
                 switch (filter) {
                     case NONE:            return null;
                     case WRITABLE_RESULT: preconstructed = true;  sentinel = result(); break;
@@ -160,6 +164,11 @@ abstract class MetadataVisitor<R> {
                     // Should never happen, unless this method is invoked concurrently in
another thread.
                     throw new ConcurrentModificationException();
                 }
+                /*
+                 * Name of properties walked from root node to the node being visited by
current method invocation.
+                 * The path is provided by `propertyPath` and the number of valid elements
is given by `nestedCount`,
+                 * which is 1 during the visit of first element.
+                 */
                 if (nestedCount >= propertyPath.length) {
                     propertyPath = Arrays.copyOf(propertyPath, nestedCount * 2);
                 }
@@ -173,6 +182,10 @@ abstract class MetadataVisitor<R> {
                      */
                     allowNull = Semaphores.queryAndSet(Semaphores.NULL_COLLECTION);
                 }
+                /*
+                 * Actual visiting. The `accessor.walk(this, metadata)` method calls below
will callback the abstract
+                 * `visit(type, value)` method declared in this class.
+                 */
                 try {
                     switch (filter) {
                         case NON_EMPTY:       accessor.walkReadable(this, metadata); break;
@@ -186,6 +199,10 @@ abstract class MetadataVisitor<R> {
                     throw new MetadataVisitorException(Arrays.copyOf(propertyPath, nestedCount),
accessor.type, e);
                 } finally {
                     if (--nestedCount == 0) {
+                        /*
+                         * We are back to the root metadata (i.e. we finished walking through
all children).
+                         * Clear thread local variables, which should restore them to their
initial value.
+                         */
                         if (!allowNull) {
                             Semaphores.clear(Semaphores.NULL_COLLECTION);
                         }
@@ -193,6 +210,10 @@ abstract class MetadataVisitor<R> {
                         if (creator != null) creator.remove();
                     }
                 }
+                /*
+                 * Cache the result in case this node is visited again (e.g. if the metadata
graph contains
+                 * cycles or if the same child node is referenced from many places).
+                 */
                 final R result = preconstructed ? sentinel : result();
                 if (visited.put(metadata, result) != sentinel) {
                     throw new ConcurrentModificationException();
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 1c06835..40a0add 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
@@ -86,7 +86,7 @@ import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
  * }
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.3
  * @module
  */
@@ -203,6 +203,13 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         private State(final byte code) {
             this.code = code;
         }
+
+        /**
+         * Whether this enumeration represents a state where data can not be modified anymore.
+         */
+        final boolean isUnmodifiable() {
+            return code >= ModifiableMetadata.FINAL;
+        }
     }
 
     /**
@@ -261,7 +268,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * The effect of invoking this method may be recursive. For example transitioning to
{@link State#FINAL}
      * implies transitioning all children {@code ModifiableMetadata} instances to the final
state too.
      *
-     * @param  target  the desired new state.
+     * @param  target  the desired new state (editable, completable or final).
      * @return {@code true} if the state of this {@code ModifiableMetadata} changed as a
result of this method call.
      * @throws UnmodifiableMetadataException if a transition to a less restrictive state
      *         (e.g. from {@link State#FINAL} to {@link State#EDITABLE}) was attempted.
@@ -287,6 +294,63 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
     }
 
     /**
+     * Copies (if necessary) this metadata and all its children.
+     * Changes in the returned metadata will not affect this {@code ModifiableMetadata} instance,
and conversely.
+     * The returned metadata will be in the {@linkplain #state() state} specified by the
{@code target} argument.
+     * The state of this {@code ModifiableMetadata} instance stay unchanged.
+     *
+     * <p>As a special case, this method returns {@code this} if and only if the specified
target is {@link State#FINAL}
+     * and this {@code ModifiableMetadata} instance is already in final state. In that particular
case, copies are not
+     * needed for protecting metadata against changes because neither {@code this} or the
returned value can be modified.</p>
+     *
+     * <p>This method is typically invoked for getting a modifiable metadata from an
unmodifiable one:</p>
+     *
+     * {@preformat java
+     *     Metadata source  = ...;          // Any implementation.
+     *     DefaultMetadata md = DefaultMetadata.castOrCopy(source);
+     *     md = (DefaultMetadata) md.deepCopy(DefaultMetadata.State.EDITABLE);
+     * }
+     *
+     * <h4>Alternative</h4>
+     * If unconditional copy is desired, or if the metadata to copy may be arbitrary implementations
+     * of GeoAPI interfaces (i.e. not necessarily a {@code ModifiableMetadata} subclass),
+     * then the following code can be used instead:
+     *
+     * {@preformat java
+     *     MetadataCopier copier = new MetadataCopier(MetadataStandard.ISO_19115);
+     *     Metadata source = ...;                           // Any implementation.
+     *     Metadata copy = copier.copy(Metadata.class, source);
+     * }
+     *
+     * The {@code Metadata} type in above example can be replaced by any other ISO 19115
type.
+     * Types from other standards can also be used if the {@link MetadataStandard#ISO_19115}
constant
+     * is replaced accordingly.
+     *
+     * @param  target  the desired state (editable, completable or final).
+     * @return a copy (except in above-cited special case) of this metadata in the specified
state.
+     *
+     * @see MetadataCopier
+     *
+     * @since 1.1
+     */
+    public ModifiableMetadata deepCopy(final State target) {
+        final MetadataCopier copier;
+        if (target.isUnmodifiable()) {
+            if (state >= FINAL) {
+                return this;
+            }
+            copier = MetadataCopier.forModifiable(getStandard());
+        } else {
+            copier = new MetadataCopier(getStandard());
+        }
+        final ModifiableMetadata md = (ModifiableMetadata) copier.copyRecursively(getInterface(),
this);
+        if (target.code > EDITABLE) {
+            md.transitionTo(target);
+        }
+        return md;
+    }
+
+    /**
      * Checks if changes in the metadata are allowed. All {@code setFoo(…)} methods in
subclasses
      * shall invoke this method (directly or indirectly) before to apply any change.
      * The current property value should be specified in argument.
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
index f9305cf..15b462c 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
@@ -32,9 +32,6 @@ import org.apache.sis.metadata.iso.identification.DefaultRepresentativeFraction;
 /**
  * Invokes {@link ModifiableMetadata#transitionTo(ModifiableMetadata.State)} recursively
on metadata elements.
  *
- * As of Apache SIS 1.0, this class is used only for {@link ModifiableMetadata.State#FINAL}.
- * But a future version may use this object for other states too.
- *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   0.3
@@ -43,9 +40,9 @@ import org.apache.sis.metadata.iso.identification.DefaultRepresentativeFraction;
 final class StateChanger extends MetadataVisitor<Boolean> {
     /**
      * The {@code StateChanger} instance in current use. The clean way would have been to
pass
-     * the instance in argument to all {@code apply(State.FINAL)} methods in metadata packages.
-     * But above-cited methods are public, and we do not want to expose {@code StateChanger}
-     * in public API. This thread-local is a workaround for that situation.
+     * the instance in argument to {@link ModifiableMetadata#transitionTo(ModifiableMetadata.State)}.
+     * But above-cited method ix public and we do not want to expose {@code StateChanger}
in public API.
+     * This thread-local is a workaround for that situation.
      */
     private static final ThreadLocal<StateChanger> VISITORS = ThreadLocal.withInitial(StateChanger::new);
 
@@ -66,7 +63,13 @@ final class StateChanger extends MetadataVisitor<Boolean> {
     }
 
     /**
-     * Applies a state change on the given metadata object.
+     * Applies a state change on the given metadata object. This is the implementation
+     * {@link ModifiableMetadata#transitionTo(ModifiableMetadata.State)} public method.
+     *
+     * <p>This is conceptually an instance (non-static) method. But the {@code this}
value is not known
+     * by the caller, because doing otherwise would force us to give public visibility to
classes that we
+     * want to keep package-private. The {@link #VISITORS} thread local variable is used
as a workaround
+     * for providing {@code this} instance without making {@code StateChanger} public.</p>
      */
     static void applyTo(final ModifiableMetadata.State target, final ModifiableMetadata metadata)
{
         final StateChanger changer = VISITORS.get();
@@ -78,6 +81,8 @@ final class StateChanger extends MetadataVisitor<Boolean> {
 
     /**
      * Returns the thread-local variable that created this {@code StateChanger} instance.
+     * {@link ThreadLocal#remove()} will be invoked after {@link MetadataVisitor} finished
+     * to walk through the given metadata and all its children.
      */
     @Override
     final ThreadLocal<StateChanger> creator() {
@@ -108,7 +113,7 @@ final class StateChanger extends MetadataVisitor<Boolean> {
     }
 
     /**
-     * Recursively change the state of all elements in the given array.
+     * Recursively changes the state of all elements in the given array.
      */
     private void applyToAll(final Object[] array) throws CloneNotSupportedException {
         for (int i=0; i < array.length; i++) {
@@ -144,8 +149,10 @@ final class StateChanger extends MetadataVisitor<Boolean> {
             return object;
         }
         if (object instanceof DefaultRepresentativeFraction) {
-            ((DefaultRepresentativeFraction) object).freeze();
-            return object;
+            if (target.isUnmodifiable()) {
+                ((DefaultRepresentativeFraction) object).freeze();
+                return object;
+            }
         }
         /*
          * CASE 2 - The object is a collection. All elements are replaced by their
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
b/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
index f40095b..864fad2 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
@@ -35,7 +35,7 @@ import static org.apache.sis.test.Assert.*;
  * This class uses {@link DefaultMedium} as an arbitrary metadata implementation for running
the tests.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   1.0
  * @module
  */
@@ -185,4 +185,44 @@ public final strictfp class ModifiableMetadataTest extends TestCase {
         }
         assertPropertiesEqual(null, "The original note.");
     }
+
+    /**
+     * Tests {@link ModifiableMetadata#deepCopy(ModifiableMetadata.State)}.
+     */
+    @Test
+    public void testDeepCopy() {
+        /*
+         * Make one of `DefaultMedium` property unmodifiable
+         * for testing `deepCopy(…)` decision to copy or not.
+         */
+        assertTrue(((DefaultIdentifier) md.getIdentifier()).transitionTo(ModifiableMetadata.State.FINAL));
+        /*
+         * Test the request for an editable copy. All children
+         * should be copied, including the unmodifiable child.
+         */
+        DefaultMedium copy = (DefaultMedium) md.deepCopy(ModifiableMetadata.State.EDITABLE);
+        assertEquals (md, copy);
+        assertNotSame(md, copy);
+        assertNotSame(md.getIdentifier(), copy.getIdentifier());
+        assertSame   (md.getMediumNote(), copy.getMediumNote());                // Not a
cloneable property.
+        assertEquals(ModifiableMetadata.State.FINAL, identifierState());
+        assertEquals(ModifiableMetadata.State.EDITABLE, ((DefaultIdentifier) copy.getIdentifier()).state());
+        /*
+         * Test the request for an unmodifiable copy. This time,
+         * the unmodifiable children should not be copied.
+         */
+        copy = (DefaultMedium) md.deepCopy(ModifiableMetadata.State.FINAL);
+        assertEquals (md, copy);
+        assertNotSame(md, copy);
+        assertSame   (md.getIdentifier(), copy.getIdentifier());                // Not copied
because unmodifiable.
+        assertSame   (md.getMediumNote(), copy.getMediumNote());                // Not a
cloneable property.
+        assertEquals(ModifiableMetadata.State.FINAL, identifierState());
+        assertEquals(ModifiableMetadata.State.FINAL, ((DefaultIdentifier) copy.getIdentifier()).state());
+        /*
+         * Special case when all metadata at play are unmodifiable.
+         */
+        md.transitionTo(ModifiableMetadata.State.FINAL);
+        copy = (DefaultMedium) md.deepCopy(ModifiableMetadata.State.FINAL);
+        assertSame(md, copy);
+    }
 }


Mime
View raw message