Author: desruisseaux
Date: Wed Sep  6 13:52:35 2017
New Revision: 1807488

URL: http://svn.apache.org/viewvc?rev=1807488&view=rev
Log:
Give some more control on the way to merge metadata elements in a collection.

Modified:
    
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
    
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
    
sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/MergerTest.java

Modified: 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java?rev=1807488&r1=1807487&r2=1807488&view=diff
==============================================================================
--- 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
 [UTF-8] (original)
+++ 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Merger.java
 [UTF-8] Wed Sep  6 13:52:35 2017
@@ -44,19 +44,19 @@ import org.apache.sis.util.Classes;
  *   <li>Otherwise if the target value is a collection, then:
  *     <ul>
  *       <li>For each element of the source collection, a corresponding 
element of the target collection is searched.
- *         A pair of source and target elements is established if the pair 
meet all of the following conditions:
+ *         A pair of source and target elements is established if the pair 
meets all of the following conditions:
  *         <ul>
  *           <li>The {@linkplain MetadataStandard#getInterface(Class) standard 
type} of the source element
  *               is assignable to the type of the target element.</li>
- *           <li>There is no conflict, i.e. no property value that are not 
collection and not equal
- *               (note: this condition is disabled if {@link #avoidConflicts} 
is {@code false}).</li>
+ *           <li>There is no conflict, i.e. no property value that are not 
collection and not equal.
+ *               This condition can be modified by overriding {@link 
#resolve(Object, ModifiableMetadata)}.</li>
  *         </ul>
  *         If such pair is found, then the merge operation if performed 
recursively
  *         for that pair of source and target elements.</li>
  *       <li>All other source elements will be added as new elements in the 
target collection.</li>
  *     </ul>
  *   </li>
- *   <li>Otherwise the {@link #unmerged unmerged(…)} method is invoked.</li>
+ *   <li>Otherwise the {@link #copy(Object, ModifiableMetadata) copy(…)} 
method is invoked.</li>
  * </ul>
  *
  * @author  Johann Sorel (Geomatys)
@@ -79,14 +79,6 @@ public class Merger {
     protected final Locale locale;
 
     /**
-     * {@code true} for performing a greater effort of avoiding merge 
conflicts.
-     * The default value is {@code false}, which may cause {@link #unmerged 
unmerged(…)} to be invoked in
-     * situation where it could have been avoided. Setting this value to 
{@code true} increase the chances
-     * of merge success at the expense of more computations.
-     */
-    public boolean avoidConflicts;
-
-    /**
      * Creates a new merger.
      *
      * @param  locale  the locale to use for formatting error messages, or 
{@code null} for the default locale.
@@ -122,15 +114,15 @@ public class Merger {
      *         for example because the source class is a more specialized type 
than the target class.
      * @throws IllegalArgumentException if this method detects a 
cross-reference between source and target metadata.
      */
-    public final void merge(final Object source, final ModifiableMetadata 
target) {
-        if (!merge(source, target, false)) {
+    public final void copy(final Object source, final ModifiableMetadata 
target) {
+        if (!copy(source, target, false)) {
             throw new 
InvalidMetadataException(errors().getString(Errors.Keys.IllegalArgumentClass_3, 
"target",
                     target.getStandard().getInterface(source.getClass()), 
Classes.getClass(target)));
         }
     }
 
     /**
-     * Implementation of {@link #merge(Object, ModifiableMetadata)} method,
+     * Implementation of {@link #copy(Object, ModifiableMetadata)} method,
      * to be invoked recursively for all child properties to merge.
      *
      * @param  dryRun  {@code true} for executing the merge operation in "dry 
run" mode instead than performing the
@@ -139,7 +131,8 @@ public class Merger {
      * @return {@code true} if the merge operation is valid, or {@code false} 
if the given arguments are valid
      *         metadata but the merge operation can nevertheless not be 
executed because it could cause data lost.
      */
-    private boolean merge(final Object source, final ModifiableMetadata 
target, final boolean dryRun) {
+    @SuppressWarnings("fallthrough")
+    private boolean copy(final Object source, final ModifiableMetadata target, 
final boolean dryRun) {
         /*
          * Verify if the given source can be merged with the target. If this 
is not the case, action
          * taken will depend on the caller: it may either skips the value or 
throws an exception.
@@ -191,7 +184,7 @@ public class Merger {
                                                : 
targetMap.putIfAbsent(propertyName, sourceValue);
             if (targetValue != null) {
                 if (targetValue instanceof ModifiableMetadata) {
-                    success = merge(sourceValue, (ModifiableMetadata) 
targetValue, dryRun);
+                    success = copy(sourceValue, (ModifiableMetadata) 
targetValue, dryRun);
                     if (!success) {
                         /*
                          * This exception may happen if the source is a 
subclass of the target. This is the converse
@@ -223,17 +216,22 @@ public class Merger {
                     for (final Object element : targetList) {
                         if (element instanceof ModifiableMetadata) {
                             final Iterator<?> it = sourceList.iterator();
-                            while (it.hasNext()) {
+distribute:                 while (it.hasNext()) {
                                 final Object value = it.next();
-                                if (!avoidConflicts || merge(value, 
(ModifiableMetadata) element, true)) {
-                                    /*
-                                     * If enabled, above 'merge' call verified 
that the merge can be done, including
-                                     * by recursive checks in all children. 
The intend is to have a "all or nothing"
-                                     * behavior, before the 'merge' call below 
starts to modify the values.
-                                     */
-                                    if (merge(value, (ModifiableMetadata) 
element, false)) {
+                                switch (resolve(value, (ModifiableMetadata) 
element)) {
+                                //  case SEPARATE: do nothing.
+                                    case MERGE: {
+                                        /*
+                                         * If enabled, copy(…, true) call 
verified that the merge can be done, including
+                                         * by recursive checks in all 
children. The intend is to have a "all or nothing"
+                                         * behavior, before the copy(…, false) 
call below starts to modify the values.
+                                         */
+                                        if (!copy(value, (ModifiableMetadata) 
element, false)) break;
+                                        // Fall through
+                                    }
+                                    case IGNORE: {
                                         it.remove();
-                                        break;          // Merge at most one 
source element to each target element.
+                                        break distribute;   // Merge at most 
one source element to each target element.
                                     }
                                 }
                             }
@@ -263,8 +261,8 @@ public class Merger {
                     success = targetValue.equals(sourceValue);
                     if (!success) {
                         if (dryRun) break;
-                        unmerged(target, propertyName, sourceValue, 
targetValue);
-                        success = true;  // If no exception has been thrown by 
'unmerged', assume the conflict solved.
+                        merge(target, propertyName, sourceValue, targetValue);
+                        success = true;  // If no exception has been thrown by 
'merged', assume the conflict solved.
                     }
                 }
             }
@@ -278,19 +276,74 @@ public class Merger {
     }
 
     /**
-     * Invoked when a metadata value can not be merged.
+     * The action to perform when a <var>source</var> metadata element is 
about to be written in an existing
+     * <var>target</var> element. Many metadata elements defined by ISO 19115 
allows multi-occurrence, i.e.
+     * are stored in {@link Collection}. When a value <var>A</var> is about to 
be added in an existing collection
+     * which already contains values <var>B</var> and <var>C</var>, then 
different scenarios are possible.
+     *
+     * <p>For <var>A</var> ⟶ {<var>B</var>, <var>C</var>}:</p>
+     * <ul>
+     *   <li>Value <var>A</var> may overwrite some values of <var>B</var>. 
This action is executed if
+     *       <code>{@linkplain Merger#resolve Merger.resolve}(A, B)</code> 
returns {@link #MERGE}.</li>
+     *   <li>Value <var>A</var> may overwrite some values of <var>C</var>. 
This action is executed if
+     *       <code>{@linkplain Merger#resolve Merger.resolve}(A, B)</code> 
returns {@link #SEPARATE},
+     *       then {@code Merger.resolve(A, C)} returns {@link #MERGE}.</li>
+     *   <li>Value <var>A</var> may be added as a new value after <var>B</var> 
and <var>C</var>.
+     *       This action is executed if <code>{@linkplain Merger#resolve 
Merger.resolve}(A, B)</code>
+     *       <strong>and</strong> {@code Merger.resolve(A, C)} return {@link 
#SEPARATE}.</li>
+     *   <li>Value <var>A</var> may be discarded. This action is executed if
+     *       <code>{@linkplain Merger#resolve Merger.resolve}(A, B)</code>
+     *       <strong>or</strong> {@code Merger.resolve(A, C)} return {@link 
#IGNORE}.</li>
+     * </ul>
+     *
+     * @see Merger#resolve(Object, ModifiableMetadata)
+     */
+    public enum Resolution {
+        /**
+         * Indicates that <var>source</var> values should be written in 
<var>target</var> attributes of existing
+         * metadata element. No new metadata object is created. If a value 
already exists in the target metadata,
+         * then the {@link Merger#merge(ModifiableMetadata, String, Object, 
Object) merge(…)} method will be invoked.
+         */
+        MERGE,
+
+        /**
+         * Indicates that <var>source</var> values should be written in 
another metadata element.
+         */
+        SEPARATE,
+
+        /**
+         * Indicates that <var>source</var> values should be discarded.
+         */
+        IGNORE
+    }
+
+    /**
+     * Invoked when a source metadata element is about to be written in an 
existing target element.
+     * The default implementation returns {@link Resolution#MERGE} if writing 
in the given target
+     * would only fill holes, without overwriting any existing value. 
Otherwise this method returns
+     * {@code Resolution#SEPARATE}.
+     *
+     * @param  source  the source metadata to copy.
+     * @param  target  where the source metadata would be copied if this 
method returns {@link Resolution#MERGE}.
+     * @return {@link Resolution#MERGE} for writing {@code source} into {@code 
target}, or
+     *         {@link Resolution#SEPARATE} for writing {@code source} in a 
separated metadata element, or
+     *         {@link Resolution#IGNORE} for discarding {@code source}.
+     */
+    protected Resolution resolve(Object source, ModifiableMetadata target) {
+        return copy(source, target, true) ? Resolution.MERGE : 
Resolution.SEPARATE;
+    }
+
+    /**
+     * Invoked when {@code Merger} can not merge a metadata value by itself.
      * The default implementation throws an {@link InvalidMetadataException}.
      * Subclasses can override this method if they want to perform a different 
processing.
      *
-     * <p><b>Tip:</b> to reduce the risks that this method is invoked, 
consider setting the
-     * {@link #avoidConflicts} flag to {@code true} before invoking {@link 
#merge merge(…)}.</p>
-     *
      * @param target        the metadata instance in which the value should 
have been written.
      * @param propertyName  the name of the property to write.
      * @param sourceValue   the value to write.
      * @param targetValue   the value that already exist in the target 
metadata.
      */
-    protected void unmerged(ModifiableMetadata target, String propertyName, 
Object sourceValue, Object targetValue) {
+    protected void merge(ModifiableMetadata target, String propertyName, 
Object sourceValue, Object targetValue) {
         throw new 
InvalidMetadataException(errors().getString(Errors.Keys.ValueAlreadyDefined_1, 
name(target, propertyName)));
     }
 }

Modified: 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java?rev=1807488&r1=1807487&r2=1807488&view=diff
==============================================================================
--- 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
 [UTF-8] (original)
+++ 
sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataCopier.java
 [UTF-8] Wed Sep  6 13:52:35 2017
@@ -47,8 +47,8 @@ import org.apache.sis.util.collection.Co
  * <p>This class is not thread-safe.
  * In multi-threads environment, each thread should use its own {@code 
MetadataCopier} instance.</p>
  *
- * <div class="section">Recommended alternative</div>
- * Deep metadata copies are sometime useful when using an existing metadata as 
a template.
+ * <div class="note"><b>Recommended alternative:</b>
+ * deep metadata copies are sometime useful when using an existing metadata as 
a template.
  * But the {@link ModifiableMetadata#unmodifiable()} method may provide a 
better way to use a metadata as a template,
  * as it returns a snapshot and allows the caller to continue to modify the 
original metadata object and create new
  * snapshots. Example:
@@ -77,6 +77,7 @@ import org.apache.sis.util.collection.Co
  * result of a call to {@link 
org.apache.sis.metadata.sql.MetadataSource#lookup(Class, String)}) into 
instances of the
  * public {@link AbstractMetadata} subclasses. But note that shallow copies as 
provided by the {@code castOrCopy(…)}
  * static methods in each {@code AbstractMetadata} subclass are sometime 
sufficient.</p>
+ * </div>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8

Modified: 
sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/MergerTest.java
URL: 
http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/MergerTest.java?rev=1807488&r1=1807487&r2=1807488&view=diff
==============================================================================
--- 
sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/MergerTest.java
 [UTF-8] (original)
+++ 
sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/MergerTest.java
 [UTF-8] Wed Sep  6 13:52:35 2017
@@ -100,8 +100,7 @@ public final strictfp class MergerTest e
         final DefaultMetadata source = createSample1();
         final DefaultMetadata target = createSample2();
         final Merger merger = new Merger(null);
-        merger.avoidConflicts = true;
-        merger.merge(source, target);
+        merger.copy(source, target);
 
         assertSetEquals(Arrays.asList(Locale.JAPANESE, Locale.FRENCH),  
target.getLanguages());
         assertSetEquals(Collections.singleton(StandardCharsets.UTF_16), 
target.getCharacterSets());


Reply via email to