> updateActions) {
if (this.errorCallback != null) {
this.errorCallback.accept(exception, Optional.ofNullable(newResourceDraft),
Optional.ofNullable(oldResource), updateActions);
@@ -125,7 +127,6 @@ public void applyErrorCallback(@Nonnull final SyncException exception, @Nullable
}
/**
- *
* @param syncException {@link Throwable} instance to supply as first param to the {@code errorCallback} function.
* @see #applyErrorCallback(SyncException exception, Object oldResource, Object newResource, List updateActions)
*/
@@ -134,7 +135,6 @@ public void applyErrorCallback(@Nonnull final SyncException syncException) {
}
/**
- *
* @param errorMessage the error message to supply as part of first param to the {@code errorCallback} function.
* @see #applyErrorCallback(SyncException exception, Object oldResource, Object newResource, List updateActions)
*/
@@ -148,9 +148,10 @@ public void applyErrorCallback(@Nonnull final String errorMessage) {
* batches and then processed. It allows to reduce the query size for fetching all resources processed in one
* batch.
* E.g. value of 30 means that 30 entries from input list would be accumulated and one API call will be performed
- * for fetching entries responding to them. Then comparision and sync are performed.
+ * for fetching entries responding to them. Then comparison and sync are performed.
*
* This batch size is set to 30 by default.
+ *
* @return option that indicates capacity of batch of resources to process.
*/
public int getBatchSize() {
@@ -164,8 +165,8 @@ public int getBatchSize() {
* generated list of update actions to produce a resultant list after the filter function has been applied.
*
* @return the {@code beforeUpdateCallback} {@link TriFunction}<{@link List}<{@link UpdateAction}<
- * {@code U}>>, {@code V}, {@code U}, {@link List}<{@link UpdateAction}<{@code U}>>>
- * function set to {@code this} {@link BaseSyncOptions}.
+ * {@code U}>>, {@code V}, {@code U}, {@link List}<{@link UpdateAction}<{@code U}>>>
+ * function set to {@code this} {@link BaseSyncOptions}.
*/
@Nullable
public TriFunction>, V, U, List>> getBeforeUpdateCallback() {
@@ -179,7 +180,7 @@ public TriFunction>, V, U, List>> getBefore
* function has been applied.
*
* @return the {@code beforeUpdateCallback} {@link Function}<{@code V}, {@link Optional}<{@code V}>>
- * function set to {@code this} {@link BaseSyncOptions}.
+ * function set to {@code this} {@link BaseSyncOptions}.
*/
@Nullable
public Function getBeforeCreateCallback() {
@@ -193,13 +194,13 @@ public Function getBeforeCreateCallback() {
* is null or {@code updateActions} is empty, this method does nothing to the supplied list of {@code updateActions}
* and returns the same list. If the result of the callback is null, an empty list is returned.
*
- * @param updateActions the list of update actions to apply the {@code beforeUpdateCallback} function on.
+ * @param updateActions the list of update actions to apply the {@code beforeUpdateCallback} function on.
* @param newResourceDraft the new resource draft that is being compared to the old resource.
- * @param oldResource the old resource that is being compared to the new draft.
+ * @param oldResource the old resource that is being compared to the new draft.
* @return a list of update actions after applying the {@code beforeUpdateCallback} function on. If the
- * {@code beforeUpdateCallback} function is null or {@code updateActions} is empty, the supplied list of
- * {@code updateActions} is returned as is. If the return of the callback is null, an empty list is
- * returned.
+ * {@code beforeUpdateCallback} function is null or {@code updateActions} is empty, the supplied list of
+ * {@code updateActions} is returned as is. If the return of the callback is null, an empty list is
+ * returned.
*/
@Nonnull
public List> applyBeforeUpdateCallback(@Nonnull final List> updateActions,
@@ -224,12 +225,12 @@ public List> applyBeforeUpdateCallback(@Nonnull final List applyBeforeCreateCallback(@Nonnull final V newResourceDraft) {
return ofNullable(
- beforeCreateCallback != null ? beforeCreateCallback.apply(newResourceDraft) : newResourceDraft);
+ beforeCreateCallback != null ? beforeCreateCallback.apply(newResourceDraft) : newResourceDraft);
}
}
diff --git a/src/main/java/com/commercetools/sync/commons/utils/AssetsUpdateActionUtils.java b/src/main/java/com/commercetools/sync/commons/utils/AssetsUpdateActionUtils.java
index 96ec13c504..bb59e8a1a8 100644
--- a/src/main/java/com/commercetools/sync/commons/utils/AssetsUpdateActionUtils.java
+++ b/src/main/java/com/commercetools/sync/commons/utils/AssetsUpdateActionUtils.java
@@ -150,7 +150,6 @@ private static List> buildAssetsUpdateAc
// It is important to have a changeAssetOrder action before an addAsset action, since changeAssetOrder requires
// asset ids for sorting them, and new assets don't have ids yet since they are generated
// by CTP after an asset is created. Therefore, the order of update actions must be:
- // removeAsset → changeAssetOrder → addAsset
//1. Remove or compare if matching.
final List> updateActions =
diff --git a/src/main/java/com/commercetools/sync/customobjects/CustomObjectSync.java b/src/main/java/com/commercetools/sync/customobjects/CustomObjectSync.java
new file mode 100644
index 0000000000..39e526ad21
--- /dev/null
+++ b/src/main/java/com/commercetools/sync/customobjects/CustomObjectSync.java
@@ -0,0 +1,342 @@
+package com.commercetools.sync.customobjects;
+
+import com.commercetools.sync.commons.BaseSync;
+import com.commercetools.sync.commons.exceptions.SyncException;
+import com.commercetools.sync.customobjects.helpers.CustomObjectCompositeIdentifier;
+import com.commercetools.sync.customobjects.helpers.CustomObjectSyncStatistics;
+import com.commercetools.sync.customobjects.utils.CustomObjectSyncUtils;
+import com.commercetools.sync.services.CustomObjectService;
+import com.commercetools.sync.services.impl.CustomObjectServiceImpl;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.sphere.sdk.customobjects.CustomObject;
+import io.sphere.sdk.customobjects.CustomObjectDraft;
+import org.apache.commons.lang3.tuple.ImmutablePair;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+import static com.commercetools.sync.commons.utils.SyncUtils.batchElements;
+import static java.lang.String.format;
+import static java.util.Optional.ofNullable;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toMap;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * This class syncs custom object drafts with the corresponding custom objects in the CTP project.
+ */
+public class CustomObjectSync extends BaseSync,
+ CustomObjectSyncStatistics, CustomObjectSyncOptions> {
+
+ private static final String CTP_CUSTOM_OBJECT_FETCH_FAILED =
+ "Failed to fetch existing custom objects with keys: '%s'.";
+ private static final String CTP_CUSTOM_OBJECT_UPDATE_FAILED =
+ "Failed to update custom object with key: '%s'. Reason: %s";
+ private static final String CTP_CUSTOM_OBJECT_CREATE_FAILED =
+ "Failed to create custom object with key: '%s'. Reason: %s";
+ private static final String CUSTOM_OBJECT_DRAFT_IS_NULL = "Failed to process null custom object draft.";
+
+ private final CustomObjectService customObjectService;
+
+ public CustomObjectSync(@Nonnull final CustomObjectSyncOptions syncOptions) {
+ this(syncOptions, new CustomObjectServiceImpl(syncOptions));
+ }
+
+ /**
+ * Takes a {@link CustomObjectSyncOptions} and a {@link CustomObjectService} instances to instantiate
+ * a new {@link CustomObjectSync} instance that could be used to sync customObject drafts in the CTP project
+ * specified in the injected {@link CustomObjectSyncOptions} instance.
+ *
+ * NOTE: This constructor is mainly to be used for tests where the services can be mocked and passed to.
+ *
+ * @param syncOptions the container of all the options of the sync process including the CTP project
+ * client and/or configuration and other sync-specific options.
+ * @param customObjectService the custom object service which is responsible for fetching/caching the
+ */
+ CustomObjectSync(
+ @Nonnull final CustomObjectSyncOptions syncOptions,
+ @Nonnull final CustomObjectService customObjectService) {
+
+ super(new CustomObjectSyncStatistics(), syncOptions);
+ this.customObjectService = customObjectService;
+ }
+
+ /**
+ * Iterates through the whole {@code customObjectDrafts} list and accumulates its valid drafts to batches.
+ * Every batch is then processed by {@link CustomObjectSync#processBatch(List)}.
+ *
+ *
Inherited doc:
+ * {@inheritDoc}
+ *
+ * @param customObjectDrafts {@link List} of {@link CustomObjectDraft}'s that would be synced into CTP project.
+ * @return {@link CompletionStage} with {@link CustomObjectSyncStatistics} holding statistics of all sync
+ * processes performed by this sync instance.
+ */
+ protected CompletionStage process(
+ @Nonnull final List> customObjectDrafts) {
+ final List>> batches = batchElements(
+ customObjectDrafts, syncOptions.getBatchSize());
+ return syncBatches(batches, CompletableFuture.completedFuture(statistics));
+ }
+
+ /**
+ * This method first creates a new {@link Set} of valid {@link CustomObjectDraft} elements. For more on the rules of
+ * validation, check: {@link CustomObjectSync#validateDraft(CustomObjectDraft)}. Using the resulting set of
+ * {@code validCustomObjectDrafts}, the matching custom objects in the target CTP project are fetched then the
+ * method {@link CustomObjectSync#syncBatch(Set, Set)} is called to perform the sync (update or create
+ * requests accordingly) on the target project.
+ *
+ * In case of error during of fetching of existing custom objects, the error callback will be triggered.
+ * And the sync process would stop for the given batch.
+ *
+ *
+ * @param batch batch of drafts that need to be synced
+ * @return a {@link CompletionStage} containing an instance
+ * of {@link CustomObjectSyncStatistics} which contains information about the result of syncing the supplied
+ * batch to the target project.
+ */
+ protected CompletionStage processBatch(
+ @Nonnull final List> batch) {
+
+ final Set> validCustomObjectDrafts = batch.stream().filter(
+ this::validateDraft).collect(toSet());
+
+ if (validCustomObjectDrafts.isEmpty()) {
+ statistics.incrementProcessed(batch.size());
+ return completedFuture(statistics);
+ } else {
+ final Set identifiers = validCustomObjectDrafts.stream().map(
+ CustomObjectCompositeIdentifier::of).collect(toSet());
+
+ return customObjectService
+ .fetchMatchingCustomObjects(identifiers)
+ .handle(ImmutablePair::new)
+ .thenCompose(fetchResponse -> {
+ final Set> fetchedCustomObjects = fetchResponse.getKey();
+ final Throwable exception = fetchResponse.getValue();
+
+ if (exception != null) {
+ final String errorMessage = format(CTP_CUSTOM_OBJECT_FETCH_FAILED, identifiers);
+ handleError(errorMessage, exception, identifiers.size());
+ return CompletableFuture.completedFuture(null);
+ } else {
+ return syncBatch(fetchedCustomObjects, validCustomObjectDrafts);
+ }
+ })
+ .thenApply(ignored -> {
+ statistics.incrementProcessed(batch.size());
+ return statistics;
+ });
+ }
+ }
+
+ /**
+ * Checks if a draft is empty for further processing. If so, then returns {@code true}. Otherwise handles an error
+ * and returns {@code false}. A valid draft is a {@link CustomObjectDraft} object that is not {@code null}.
+ *
+ * @param draft nullable draft
+ * @return boolean that indicate if given {@code draft} is valid for sync
+ */
+ private boolean validateDraft(@Nullable final CustomObjectDraft draft) {
+ if (draft == null) {
+ handleError(CUSTOM_OBJECT_DRAFT_IS_NULL, null, 1);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Given a {@link String} {@code errorMessage} and a {@link Throwable} {@code exception}, this method calls the
+ * optional error callback specified in the {@code syncOptions} and updates the {@code statistics} instance by
+ * incrementing the total number of failed custom objects to sync.
+ *
+ * @param errorMessage The error message describing the reason(s) of failure.
+ * @param exception The exception that called caused the failure, if any.
+ * @param failedTimes The number of times that the failed custom objects counter is incremented.
+ */
+ private void handleError(@Nonnull final String errorMessage, @Nullable final Throwable exception,
+ final int failedTimes) {
+ SyncException syncException = exception != null ? new SyncException(errorMessage, exception)
+ : new SyncException(errorMessage);
+ syncOptions.applyErrorCallback(syncException);
+ statistics.incrementFailed(failedTimes);
+ }
+
+ /**
+ * Given a {@link String} {@code errorMessage} and a {@link Throwable} {@code exception}, this method calls the
+ * optional error callback specified in the {@code syncOptions} and updates the {@code statistics} instance by
+ * incrementing the total number of failed custom objects to sync.
+ *
+ * @param errorMessage The error message describing the reason(s) of failure.
+ * @param exception The exception that called caused the failure, if any.
+ * @param failedTimes The number of times that the failed custom objects counter is incremented.
+ * @param oldCustomObject existing custom object that could be updated.
+ * @param newCustomObjectDraft draft containing data that could differ from data in {@code oldCustomObject}.
+ */
+ private void handleError(@Nonnull final String errorMessage, @Nullable final Throwable exception,
+ final int failedTimes, @Nullable final CustomObject oldCustomObject,
+ @Nullable final CustomObjectDraft newCustomObjectDraft) {
+
+ SyncException syncException = exception != null ? new SyncException(errorMessage, exception)
+ : new SyncException(errorMessage);
+ syncOptions.applyErrorCallback(syncException, oldCustomObject, newCustomObjectDraft, null);
+ statistics.incrementFailed(failedTimes);
+ }
+
+ /**
+ * Given a set of custom object drafts, attempts to sync the drafts with the existing custom objects in the CTP
+ * project. The custom object and the draft are considered to match if they have the same key and container.
+ *
+ * @param oldCustomObjects old custom objects.
+ * @param newCustomObjectDrafts drafts that need to be synced.
+ * @return a {@link CompletionStage} which contains an empty result after execution of the update
+ */
+ @Nonnull
+ private CompletionStage syncBatch(
+ @Nonnull final Set> oldCustomObjects,
+ @Nonnull final Set> newCustomObjectDrafts) {
+
+ final Map> oldCustomObjectMap =
+ oldCustomObjects.stream().collect(
+ toMap(customObject -> CustomObjectCompositeIdentifier.of(
+ customObject.getKey(), customObject.getContainer()).toString(), identity()));
+
+ return CompletableFuture.allOf(newCustomObjectDrafts
+ .stream()
+ .map(newCustomObjectDraft -> {
+ final CustomObject oldCustomObject = oldCustomObjectMap.get(
+ CustomObjectCompositeIdentifier.of(newCustomObjectDraft).toString());
+ return ofNullable(oldCustomObject)
+ .map(customObject -> updateCustomObject(oldCustomObject, newCustomObjectDraft))
+ .orElseGet(() -> applyCallbackAndCreate(newCustomObjectDraft));
+ })
+ .map(CompletionStage::toCompletableFuture)
+ .toArray(CompletableFuture[]::new));
+ }
+
+ /**
+ * Given a custom object draft, this method applies the beforeCreateCallback and then issues a create request to the
+ * CTP project to create the corresponding CustomObject.
+ *
+ * @param customObjectDraft the custom object draft to create the custom object from.
+ * @return a {@link CompletionStage} which contains created custom object after success execution of the create.
+ * Otherwise it contains an empty result in case of failure.
+ */
+ @Nonnull
+ private CompletionStage>> applyCallbackAndCreate(
+ @Nonnull final CustomObjectDraft customObjectDraft) {
+
+ return syncOptions
+ .applyBeforeCreateCallback(customObjectDraft)
+ .map(draft -> customObjectService
+ .upsertCustomObject(draft)
+ .thenApply(customObjectOptional -> {
+ if (customObjectOptional.isPresent()) {
+ statistics.incrementCreated();
+ } else {
+ statistics.incrementFailed();
+ }
+ return customObjectOptional;
+ }).exceptionally(sphereException -> {
+ final String errorMessage =
+ format(CTP_CUSTOM_OBJECT_CREATE_FAILED,
+ CustomObjectCompositeIdentifier.of(customObjectDraft).toString(),
+ sphereException.getMessage());
+ handleError(errorMessage, sphereException, 1,
+ null, customObjectDraft);
+ return Optional.empty();
+ })
+ ).orElse(completedFuture(Optional.empty()));
+ }
+
+ /**
+ * Given an existing {@link CustomObject} and a new {@link CustomObjectDraft}, the method first checks whether
+ * existing {@link CustomObject} and a new {@link CustomObjectDraft} are identical. If so, the method aborts update
+ * the new draft and return an empty result, otherwise a request is made to CTP to update the existing custom
+ * object.
+ *
+ * The {@code statistics} instance is updated accordingly to whether the CTP request was carried
+ * out successfully or not. If an exception was thrown on executing the request to CTP,the error handling method
+ * is called.
+ *
+ * @param oldCustomObject existing custom object that could be updated.
+ * @param newCustomObject draft containing data that could differ from data in {@code oldCustomObject}.
+ * @return a {@link CompletionStage} which contains an empty result after execution of the update.
+ */
+ @Nonnull
+ private CompletionStage>> updateCustomObject(
+ @Nonnull final CustomObject oldCustomObject,
+ @Nonnull final CustomObjectDraft newCustomObject) {
+
+ if (!CustomObjectSyncUtils.hasIdenticalValue(oldCustomObject, newCustomObject)) {
+ return customObjectService
+ .upsertCustomObject(newCustomObject)
+ .handle(ImmutablePair::new)
+ .thenCompose(updatedResponseEntry -> {
+ final Optional> updateCustomObjectOptional = updatedResponseEntry.getKey();
+ final Throwable sphereException = updatedResponseEntry.getValue();
+ if (sphereException != null) {
+ return executeSupplierIfConcurrentModificationException(sphereException,
+ () -> fetchAndUpdate(oldCustomObject, newCustomObject),
+ () -> {
+ final String errorMessage =
+ format(CTP_CUSTOM_OBJECT_UPDATE_FAILED,
+ CustomObjectCompositeIdentifier.of(newCustomObject).toString(),
+ sphereException.getMessage());
+ handleError(errorMessage, sphereException, 1,
+ oldCustomObject, newCustomObject);
+ return CompletableFuture.completedFuture(Optional.empty());
+ });
+ } else {
+ statistics.incrementUpdated();
+ return CompletableFuture.completedFuture(Optional.of(updateCustomObjectOptional.get()));
+ }
+
+ });
+ }
+ return completedFuture(Optional.empty());
+ }
+
+ @Nonnull
+ private CompletionStage>> fetchAndUpdate(
+ @Nonnull final CustomObject oldCustomObject,
+ @Nonnull final CustomObjectDraft customObjectDraft) {
+
+ final CustomObjectCompositeIdentifier identifier = CustomObjectCompositeIdentifier.of(oldCustomObject);
+
+ return customObjectService
+ .fetchCustomObject(identifier)
+ .handle(ImmutablePair::new)
+ .thenCompose(fetchedResponseEntry -> {
+ final Optional> fetchedCustomObjectOptional = fetchedResponseEntry.getKey();
+ final Throwable exception = fetchedResponseEntry.getValue();
+
+ if (exception != null) {
+ final String errorMessage = format(CTP_CUSTOM_OBJECT_UPDATE_FAILED, identifier.toString(),
+ "Failed to fetch from CTP while retrying after concurrency modification.");
+ handleError(errorMessage, exception, 1, oldCustomObject, customObjectDraft);
+ return CompletableFuture.completedFuture(Optional.empty());
+ }
+ return fetchedCustomObjectOptional
+ .map(fetchedCustomObject -> updateCustomObject(fetchedCustomObject, customObjectDraft))
+ .orElseGet(() -> {
+ final String errorMessage =
+ format(CTP_CUSTOM_OBJECT_UPDATE_FAILED, identifier.toString(),
+ "Not found when attempting to fetch while retrying "
+ + "after concurrency modification.");
+ handleError(errorMessage, null, 1,
+ oldCustomObject, customObjectDraft);
+ return CompletableFuture.completedFuture(null);
+ });
+
+ });
+ }
+}
diff --git a/src/main/java/com/commercetools/sync/customobjects/CustomObjectSyncOptions.java b/src/main/java/com/commercetools/sync/customobjects/CustomObjectSyncOptions.java
index c27eae1065..04b754fbab 100644
--- a/src/main/java/com/commercetools/sync/customobjects/CustomObjectSyncOptions.java
+++ b/src/main/java/com/commercetools/sync/customobjects/CustomObjectSyncOptions.java
@@ -5,29 +5,33 @@
import com.commercetools.sync.commons.utils.QuadConsumer;
import com.commercetools.sync.commons.utils.TriConsumer;
import com.commercetools.sync.commons.utils.TriFunction;
+import com.fasterxml.jackson.databind.JsonNode;
import io.sphere.sdk.client.SphereClient;
import io.sphere.sdk.commands.UpdateAction;
import io.sphere.sdk.customobjects.CustomObject;
import io.sphere.sdk.customobjects.CustomObjectDraft;
-
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
-public final class CustomObjectSyncOptions extends BaseSyncOptions {
+public final class CustomObjectSyncOptions extends BaseSyncOptions,
+ CustomObjectDraft> {
CustomObjectSyncOptions(
@Nonnull final SphereClient ctpClient,
- @Nullable final QuadConsumer, Optional,
- List>> errorCallBack,
- @Nullable final TriConsumer, Optional>
+ @Nullable final QuadConsumer>,
+ Optional>,
+ List>>> errorCallBack,
+ @Nullable final TriConsumer>,
+ Optional>>
warningCallBack,
final int batchSize,
- @Nullable final TriFunction>, CustomObjectDraft, CustomObject,
- List>> beforeUpdateCallback,
- @Nullable final Function beforeCreateCallback) {
+ @Nullable final TriFunction>>, CustomObjectDraft,
+ CustomObject,
+ List>>> beforeUpdateCallback,
+ @Nullable final Function, CustomObjectDraft> beforeCreateCallback) {
super(
ctpClient,
errorCallBack,
@@ -36,5 +40,4 @@ public final class CustomObjectSyncOptions extends BaseSyncOptions {
+ CustomObjectSyncOptions, CustomObject, CustomObjectDraft> {
public static final int BATCH_SIZE_DEFAULT = 50;
diff --git a/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectCompositeIdentifier.java b/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectCompositeIdentifier.java
index 73b57e7e63..e5f9700b59 100644
--- a/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectCompositeIdentifier.java
+++ b/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectCompositeIdentifier.java
@@ -4,6 +4,7 @@
import io.sphere.sdk.customobjects.CustomObjectDraft;
import javax.annotation.Nonnull;
+import java.util.Objects;
import static java.lang.String.format;
@@ -19,19 +20,18 @@ public final class CustomObjectCompositeIdentifier {
private final String container;
private CustomObjectCompositeIdentifier(@Nonnull final String key,
- @Nonnull final String container) {
+ @Nonnull final String container) {
this.key = key;
this.container = container;
}
-
/**
* Given a {@link CustomObjectDraft}, creates a {@link CustomObjectCompositeIdentifier} using the following fields
* from the supplied {@link CustomObjectDraft}:
*
* - {@link CustomObjectCompositeIdentifier#key}: key of {@link CustomObjectDraft#getKey()}
* - {@link CustomObjectCompositeIdentifier#container}: container of {@link CustomObjectDraft#getContainer()}
-
+ *
*
*
* @param customObjectDraft a composite id is built using its fields.
@@ -62,7 +62,7 @@ public static CustomObjectCompositeIdentifier of(@Nonnull final CustomObject cus
* Given a {@link String} and {@link String}, creates a {@link CustomObjectCompositeIdentifier} using the following
* fields from the supplied attributes.
*
- * @param key key of CustomerObject to build composite Id.
+ * @param key key of CustomerObject to build composite Id.
* @param container container of CustomerObject to build composite Id.
* @return a composite id comprised of the fields of the supplied {@code key} and {@code container}.
*/
@@ -83,4 +83,21 @@ public String getContainer() {
public String toString() {
return format("{key='%s', container='%s'}", key, container);
}
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof CustomObjectCompositeIdentifier)) {
+ return false;
+ }
+ final CustomObjectCompositeIdentifier that = (CustomObjectCompositeIdentifier) obj;
+ return key.equals(that.key) && container.equals(that.container);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, container);
+ }
}
diff --git a/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectSyncStatistics.java b/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectSyncStatistics.java
new file mode 100644
index 0000000000..3950da3c1c
--- /dev/null
+++ b/src/main/java/com/commercetools/sync/customobjects/helpers/CustomObjectSyncStatistics.java
@@ -0,0 +1,23 @@
+package com.commercetools.sync.customobjects.helpers;
+
+import com.commercetools.sync.commons.helpers.BaseSyncStatistics;
+
+import static java.lang.String.format;
+
+public class CustomObjectSyncStatistics extends BaseSyncStatistics {
+ /**
+ * Builds a summary of the custom object sync statistics instance that looks like the following example:
+ *
+ * "Summary: 2 custom objects were processed in total (0 created, 0 updated and 0 failed to sync)."
+ *
+ * @return a summary message of the custom objects sync statistics instance.
+ */
+ @Override
+ public String getReportMessage() {
+ reportMessage = format(
+ "Summary: %s custom objects were processed in total (%s created, %s updated and %s failed to sync).",
+ getProcessed(), getCreated(), getUpdated(), getFailed());
+
+ return reportMessage;
+ }
+}
diff --git a/src/main/java/com/commercetools/sync/customobjects/utils/CustomObjectSyncUtils.java b/src/main/java/com/commercetools/sync/customobjects/utils/CustomObjectSyncUtils.java
new file mode 100644
index 0000000000..498da3941c
--- /dev/null
+++ b/src/main/java/com/commercetools/sync/customobjects/utils/CustomObjectSyncUtils.java
@@ -0,0 +1,28 @@
+package com.commercetools.sync.customobjects.utils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.sphere.sdk.customobjects.CustomObject;
+import io.sphere.sdk.customobjects.CustomObjectDraft;
+
+import javax.annotation.Nonnull;
+
+public class CustomObjectSyncUtils {
+
+ /**
+ * Compares the value of a {@link CustomObject} to the value of a {@link CustomObjectDraft}.
+ * It returns a boolean whether the values are identical or not.
+ *
+ * @param oldCustomObject the {@link CustomObject} which should be synced.
+ * @param newCustomObject the {@link CustomObjectDraft} with the new data.
+ * @return A boolean whether the value of the CustomObject and CustomObjectDraft is identical or not.
+ */
+
+ public static boolean hasIdenticalValue(
+ @Nonnull final CustomObject oldCustomObject,
+ @Nonnull final CustomObjectDraft newCustomObject) {
+ JsonNode oldValue = oldCustomObject.getValue();
+ JsonNode newValue = newCustomObject.getValue();
+
+ return oldValue.equals(newValue);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/commercetools/sync/services/CustomObjectService.java b/src/main/java/com/commercetools/sync/services/CustomObjectService.java
index 07fb83a0cc..88ed832aef 100644
--- a/src/main/java/com/commercetools/sync/services/CustomObjectService.java
+++ b/src/main/java/com/commercetools/sync/services/CustomObjectService.java
@@ -23,11 +23,11 @@ public interface CustomObjectService {
* a {@link CompletionStage}<{@link Optional}<{@link String}>>
* in which the {@link Optional} could contain the id inside of it.
*
- * @param identifier the identifier object containing CustomObject key and container, by which a
- * {@link io.sphere.sdk.customobjects.CustomObject} id should be fetched from the CTP project.
+ * @param identifier the identifier object containing CustomObject key and container, by which a
+ * {@link io.sphere.sdk.customobjects.CustomObject} id should be fetched from the CTP project.
* @return {@link CompletionStage}<{@link Optional}<{@link String}>> in which the result of its
- * completion could contain an {@link Optional} with the id inside of it or an empty {@link Optional} if no
- * {@link CustomObject} was found in the CTP project with this identifier.
+ * completion could contain an {@link Optional} with the id inside of it or an empty {@link Optional} if no
+ * {@link CustomObject} was found in the CTP project with this identifier.
*/
@Nonnull
@@ -42,7 +42,7 @@ public interface CustomObjectService {
* @param identifiers set of CustomObjectCompositeIdentifiers. Each identifier includes key and container to fetch
* matching CustomObject.
* @return {@link CompletionStage}<{@link Map}> in which the result of its completion contains a {@link Set}
- * of all matching CustomObjects.
+ * of all matching CustomObjects.
*/
@Nonnull
CompletionStage>> fetchMatchingCustomObjects(
@@ -56,7 +56,7 @@ CompletionStage>> fetchMatchingCustomObjects(
*
* @param identifier the identifier of the CustomObject to fetch.
* @return {@link CompletionStage}<{@link Optional}> in which the result of its completion contains an
- * {@link Optional} that contains the matching {@link CustomObject} if exists, otherwise empty.
+ * {@link Optional} that contains the matching {@link CustomObject} if exists, otherwise empty.
*/
@Nonnull
CompletionStage>> fetchCustomObject(
@@ -82,7 +82,7 @@ CompletionStage>> fetchCustomObject(
*
* @param customObjectDraft the resource draft to create or update a resource based off of.
* @return a {@link CompletionStage} containing an optional with the created/updated resource if successful
- * otherwise an empty optional.
+ * otherwise an empty optional.
*/
@Nonnull
CompletionStage>> upsertCustomObject(
diff --git a/src/main/java/com/commercetools/sync/services/impl/BaseService.java b/src/main/java/com/commercetools/sync/services/impl/BaseService.java
index 808851563d..3358c607f1 100644
--- a/src/main/java/com/commercetools/sync/services/impl/BaseService.java
+++ b/src/main/java/com/commercetools/sync/services/impl/BaseService.java
@@ -43,7 +43,7 @@
* @param Expansion Model (e.g. {@link io.sphere.sdk.products.expansion.ProductExpansionModel},
* {@link io.sphere.sdk.categories.expansion.CategoryExpansionModel}, etc..
*/
-abstract class BaseService, S extends BaseSyncOptions,
+abstract class BaseService, S extends BaseSyncOptions,
Q extends MetaModelQueryDsl, M, E> {
final S syncOptions;
@@ -67,7 +67,7 @@ abstract class BaseService, S extends BaseSyncOpt
* resource.
* @param updateActions the update actions to execute on the resource.
* @return an instance of {@link CompletionStage}<{@code U}> which contains as a result an instance of
- * the resource {@link U} after all the update actions have been executed.
+ * the resource {@link U} after all the update actions have been executed.
*/
@Nonnull
CompletionStage updateResource(
@@ -91,7 +91,7 @@ CompletionStage updateResource(
* resource.
* @param batches the batches of update actions to execute.
* @return an instance of {@link CompletionStage}<{@code U}> which contains as a result an instance of
- * the resource {@link U} after all the update actions in all batches have been executed.
+ * the resource {@link U} after all the update actions in all batches have been executed.
*/
@Nonnull
private CompletionStage updateBatches(
@@ -125,7 +125,7 @@ private CompletionStage updateBatches(
* @param keyMapper a function to get the key from the supplied draft.
* @param createCommand a function to get the create command using the supplied draft.
* @return a {@link CompletionStage} containing an optional with the created resource if successful otherwise an
- * empty optional.
+ * empty optional.
*/
@SuppressWarnings("unchecked")
@Nonnull
@@ -142,22 +142,8 @@ CompletionStage> createResource(
null, draft, null);
return CompletableFuture.completedFuture(Optional.empty());
} else {
- return syncOptions
- .getCtpClient()
- .execute(createCommand.apply(draft))
- .handle(((resource, exception) -> {
- if (exception == null) {
- keyToIdCache.put(draftKey, resource.getId());
- return Optional.of(resource);
- } else {
- syncOptions.applyErrorCallback(
- new SyncException(format(CREATE_FAILED, draftKey, exception.getMessage()), exception),
- null, draft, null);
- return Optional.empty();
- }
- }));
+ return executeCreateCommand(draft, keyMapper, createCommand);
}
-
}
/**
@@ -173,8 +159,8 @@ CompletionStage> createResource(
* @param keyMapper a function to get the key from the resource.
* @param querySupplier supplies the query to fetch the resource with the given key.
* @return {@link CompletionStage}<{@link Optional}<{@link String}>> in which the result of it's
- * completion could contain an {@link Optional} with the id inside of it or an empty {@link Optional} if no
- * resource was found in the CTP project with this key.
+ * completion could contain an {@link Optional} with the id inside of it or an empty {@link Optional} if no
+ * resource was found in the CTP project with this key.
*/
@Nonnull
CompletionStage> fetchCachedResourceId(
@@ -247,7 +233,7 @@ CompletionStage