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());