Skip to content

Commit

Permalink
#325: Move createResources services out of services to sync.
Browse files Browse the repository at this point in the history
  • Loading branch information
heshamMassoud committed Nov 24, 2018
1 parent b75931c commit 9cc4b57
Show file tree
Hide file tree
Showing 9 changed files with 40 additions and 271 deletions.
Expand Up @@ -231,98 +231,6 @@ public void fetchCachedCategoryId_WithNonExistingCategory_ShouldNotFetchACategor
assertThat(errorCallBackMessages).isEmpty();
}

@Test
public void createCategories_WithAllValidCategories_ShouldCreateCategories() {
final CategoryDraft categoryDraft1 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture1"),
LocalizedString.of(Locale.ENGLISH, "classic-furniture1", Locale.GERMAN, "klassische-moebel1"))
.key("key1")
.build();

final CategoryDraft categoryDraft2 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture2"),
LocalizedString.of(Locale.ENGLISH, "classic-furniture2", Locale.GERMAN, "klassische-moebel2"))
.key("key2")
.build();

final Set<CategoryDraft> categoryDrafts = new HashSet<>();
categoryDrafts.add(categoryDraft1);
categoryDrafts.add(categoryDraft2);

final Set<Category> createdCategories = categoryService.createCategories(categoryDrafts)
.toCompletableFuture().join();

assertThat(createdCategories).hasSize(2);
assertThat(errorCallBackExceptions).isEmpty();
assertThat(errorCallBackMessages).isEmpty();
}

@Test
public void createCategories_WithSomeValidCategories_ShouldCreateCategoriesAndTriggerCallBack() {
// Draft with invalid key
final CategoryDraft categoryDraft1 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture1"),
LocalizedString.of(Locale.ENGLISH, "classic-furniture1", Locale.GERMAN, "klassische-moebel1"))
.key("1")
.build();

final CategoryDraft categoryDraft2 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture2"),
LocalizedString.of(Locale.ENGLISH, "classic-furniture2", Locale.GERMAN, "klassische-moebel2"))
.key("key2")
.build();

final Set<CategoryDraft> categoryDrafts = new HashSet<>();
categoryDrafts.add(categoryDraft1);
categoryDrafts.add(categoryDraft2);

final Set<Category> createdCategories = categoryService.createCategories(categoryDrafts)
.toCompletableFuture().join();

assertThat(errorCallBackExceptions).hasSize(1);
assertThat(errorCallBackMessages).hasSize(1);
assertThat(errorCallBackMessages.get(0)).contains("Invalid key '1'. Keys may only contain alphanumeric"
+ " characters, underscores and hyphens and must have a minimum length of 2 characters and maximum length"
+ " of 256 characters.");
assertThat(createdCategories).hasSize(1);
}

@Test
public void createCategories_WithNoneValidCategories_ShouldTriggerCallBack() {
// Draft with invalid key
final CategoryDraft categoryDraft1 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture1"),
LocalizedString.of(Locale.ENGLISH, "classic-furniture1", Locale.GERMAN, "klassische-moebel1"))
.key("1")
.build();

// Draft with duplicate slug
final CategoryDraft categoryDraft2 = CategoryDraftBuilder
.of(LocalizedString.of(Locale.ENGLISH, "classic furniture2"),
LocalizedString.of(Locale.ENGLISH, "furniture"))
.key("key2")
.build();

final Set<CategoryDraft> categoryDrafts = new HashSet<>();
categoryDrafts.add(categoryDraft1);
categoryDrafts.add(categoryDraft2);

final Set<Category> createdCategories = categoryService.createCategories(categoryDrafts)
.toCompletableFuture().join();

assertThat(errorCallBackExceptions).hasSize(2);
assertThat(errorCallBackMessages).hasSize(2);
// Since the order of creation is not ensured by allOf, so we assert in list of error messages (as string):
assertThat(errorCallBackMessages.toString()).contains("Invalid key '1'. Keys may only contain alphanumeric"
+ " characters, underscores and hyphens and must have a minimum length of 2 characters and maximum length"
+ " of 256 characters.");
assertThat(errorCallBackMessages.toString()).contains("\"code\" : \"DuplicateField\"");
assertThat(errorCallBackMessages.toString()).contains("\"field\" : \"slug.en\"");
assertThat(errorCallBackMessages.toString()).contains("\"duplicateValue\" : \"furniture\"");

assertThat(createdCategories).isEmpty();
}

@Test
public void fetchCachedCategoryId_WithExistingCategory_ShouldFetchCategoryAndCache() {
final Optional<String> categoryId = categoryService.fetchCachedCategoryId(oldCategory.getKey())
Expand Down
Expand Up @@ -31,7 +31,6 @@
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

import java.util.ArrayList;
Expand Down Expand Up @@ -341,134 +340,6 @@ public void fetchMatchingProductsByKeys_WithAllExistingSetOfKeys_ShouldCacheFetc
assertThat(errorCallBackMessages).isEmpty();
}

@Test
public void createProducts_WithAllValidProducts_ShouldCreateProducts() {
// create a draft based of the same existing product but with different key, slug and master variant SKU since
// these values should be unique on CTP for the product to be created.
final ProductDraft productDraft1 = createProductDraftBuilder(PRODUCT_KEY_1_RESOURCE_PATH,
productType.toReference())
.key("newKey")
.taxCategory(null)
.state(null)
.categories(emptyList())
.categoryOrderHints(null)
.slug(LocalizedString.of(Locale.ENGLISH, "newSlug"))
.masterVariant(ProductVariantDraftBuilder.of().build())
.build();

final ProductDraft productDraft2 = createProductDraft(PRODUCT_KEY_2_RESOURCE_PATH, productType.toReference(),
null, null, categoryReferencesWithIds,
createRandomCategoryOrderHints(categoryReferencesWithIds));

final Set<ProductDraft> productDrafts = new HashSet<>();
productDrafts.add(productDraft1);
productDrafts.add(productDraft2);

final Set<Product> createdProducts = productService.createProducts(productDrafts)
.toCompletableFuture().join();

assertThat(createdProducts).hasSize(2);
assertThat(errorCallBackExceptions).isEmpty();
assertThat(errorCallBackMessages).isEmpty();
}

@Test
@Ignore("Ignoring test because of bug in CTP: https://jira.commercetools.com/browse/SUPPORT-1348")
public void createProducts_WithSomeValidProducts_ShouldCreateProductsAndTriggerCallBack() {
// create a draft based of the same existing product but with different key, slug and master variant SKU since
// these values should be unique on CTP for the product to be created.
final ProductDraft productDraft1 = createProductDraftBuilder(PRODUCT_KEY_1_RESOURCE_PATH,
productType.toReference())
.key("1")
.categories(emptyList())
.categoryOrderHints(null)
.slug(LocalizedString.of(Locale.ENGLISH, "newSlug"))
.masterVariant(ProductVariantDraftBuilder.of().build())
.build();

final ProductDraft productDraft2 = createProductDraft(PRODUCT_KEY_2_RESOURCE_PATH, productType.toReference(),
null, null, categoryReferencesWithIds,
createRandomCategoryOrderHints(categoryReferencesWithIds));

final Set<ProductDraft> productDrafts = new HashSet<>();
productDrafts.add(productDraft1);
productDrafts.add(productDraft2);

final Set<Product> createdProducts = productService.createProducts(productDrafts)
.toCompletableFuture().join();

assertThat(errorCallBackExceptions).hasSize(1);
assertThat(errorCallBackMessages).hasSize(1);
assertThat(errorCallBackMessages.get(0)).contains("Invalid key '1'. Keys may only contain "
+ "alphanumeric characters, underscores and hyphens and must have a maximum length of 256 characters.");
assertThat(createdProducts).hasSize(1);
}

@Test
public void createProducts_WithNoneValidProducts_ShouldTriggerCallBack() {
// create a draft based of the same existing product but with different key, slug and master variant SKU since
// these values should be unique on CTP for the product to be created.
final ProductDraft productDraft1 = createProductDraftBuilder(PRODUCT_KEY_1_RESOURCE_PATH,
productType.toReference())
.key("newKey")
.taxCategory(null)
.state(null)
.categories(emptyList())
.categoryOrderHints(null)
.masterVariant(ProductVariantDraftBuilder.of().build())
.build();

final ProductDraft productDraft2 = createProductDraftBuilder(PRODUCT_KEY_1_RESOURCE_PATH,
productType.toReference())
.key("newKey1")
.taxCategory(null)
.state(null)
.categories(emptyList())
.categoryOrderHints(null)
.masterVariant(ProductVariantDraftBuilder.of().build())
.build();


final Set<ProductDraft> productDrafts = new HashSet<>();
productDrafts.add(productDraft1);
productDrafts.add(productDraft2);

final Set<Product> createdProducts = productService.createProducts(productDrafts)
.toCompletableFuture().join();

final String duplicatedSlug = "english-slug";
assertThat(errorCallBackExceptions)
.hasSize(2)
.allSatisfy(exception -> {
assertThat(exception).isExactlyInstanceOf(ErrorResponseException.class);
final ErrorResponseException errorResponse = ((ErrorResponseException)exception);

final List<DuplicateFieldError> fieldErrors = errorResponse
.getErrors()
.stream()
.map(sphereError -> {
assertThat(sphereError.getCode()).isEqualTo(DuplicateFieldError.CODE);
return sphereError.as(DuplicateFieldError.class);
})
.collect(toList());
assertThat(fieldErrors).hasSize(1);
assertThat(fieldErrors).allSatisfy(error -> {
assertThat(error.getField()).isEqualTo("slug.en");
assertThat(error.getDuplicateValue()).isEqualTo(duplicatedSlug);
});
});

assertThat(errorCallBackMessages)
.hasSize(2)
.allSatisfy(errorMessage -> {
assertThat(errorMessage).contains("\"code\" : \"DuplicateField\"");
assertThat(errorMessage).contains("\"field\" : \"slug.en\"");
assertThat(errorMessage).contains("\"duplicateValue\" : \"" + duplicatedSlug + "\"");
});

assertThat(createdProducts).isEmpty();
}

@Test
@SuppressWarnings("ConstantConditions")
public void createProduct_WithValidProduct_ShouldCreateProduct() {
Expand Down
22 changes: 20 additions & 2 deletions src/main/java/com/commercetools/sync/categories/CategorySync.java
Expand Up @@ -32,12 +32,14 @@

import static com.commercetools.sync.categories.helpers.CategoryReferenceResolver.getParentCategoryKey;
import static com.commercetools.sync.categories.utils.CategorySyncUtils.buildActions;
import static com.commercetools.sync.commons.utils.CompletableFutureUtils.mapValuesToFutureOfCompletedValues;
import static com.commercetools.sync.commons.utils.ResourceIdentifierUtils.toResourceIdentifierIfNotNull;
import static com.commercetools.sync.commons.utils.SyncUtils.batchElements;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

public class CategorySync extends BaseSync<CategoryDraft, CategorySyncStatistics, CategorySyncOptions> {

private static final String CATEGORY_DRAFT_KEY_NOT_SET = "CategoryDraft with name: %s doesn't have a key.";
private static final String CATEGORY_DRAFT_IS_NULL = "CategoryDraft is null.";
private static final String FAILED_TO_RESOLVE_REFERENCES = "Failed to resolve references on "
Expand Down Expand Up @@ -166,7 +168,7 @@ protected CompletionStage<CategorySyncStatistics> processBatch(@Nonnull final Li
prepareDraftsForProcessing(categoryDrafts, keyToIdCache);
categoryKeysToFetch = existingCategoryDrafts.stream().map(CategoryDraft::getKey)
.collect(Collectors.toSet());
return categoryService.createCategories(newCategoryDrafts)
return createCategories(newCategoryDrafts)
.thenAccept(this::processCreatedCategories)
.thenCompose(result -> categoryService
.fetchMatchingCategoriesByKeys(categoryKeysToFetch))
Expand All @@ -177,13 +179,29 @@ protected CompletionStage<CategorySyncStatistics> processBatch(@Nonnull final Li
updateCategoriesSequentially(categoryDraftsToUpdate))
.thenCompose(ignoredResult ->
updateCategoriesInParallel(categoryDraftsToUpdate))
.thenApply((ignoredResult) -> {
.thenApply(ignoredResult -> {
statistics.incrementProcessed(numberOfNewDraftsToProcess);
return statistics;
});
});
}

@Nonnull
private CompletionStage<Set<Category>> createCategories(@Nonnull final Set<CategoryDraft> categoryDrafts) {
return mapValuesToFutureOfCompletedValues(categoryDrafts, this::applyCallbackAndCreate)
.thenApply(results -> results.filter(Optional::isPresent).map(Optional::get))
.thenApply(createdCategories -> createdCategories.collect(Collectors.toSet()));
}

@Nonnull
private CompletionStage<Optional<Category>> applyCallbackAndCreate(@Nonnull final CategoryDraft categoryDraft) {

return syncOptions
.applyBeforeCreateCallBack(categoryDraft)
.map(categoryService::createCategory)
.orElse(CompletableFuture.completedFuture(Optional.empty()));
}

/**
* Given a list of {@code CategoryDraft} elements, this method calculates the number of drafts that need to be
* processed in this batch, given they weren't processed before, plus the number of null drafts and drafts with null
Expand Down
22 changes: 19 additions & 3 deletions src/main/java/com/commercetools/sync/products/ProductSync.java
Expand Up @@ -50,6 +50,7 @@
import static java.util.stream.Collectors.toList;

public class ProductSync extends BaseSync<ProductDraft, ProductSyncStatistics, ProductSyncOptions> {

private static final String UPDATE_FAILED = "Failed to update Product with key: '%s'. Reason: %s";
private static final String UNEXPECTED_DELETE = "Product with key: '%s' was deleted unexpectedly.";
private static final String FAILED_TO_RESOLVE_REFERENCES = "Failed to resolve references on "
Expand Down Expand Up @@ -178,16 +179,31 @@ private static Optional<Product> getProductByKeyIfExists(@Nonnull final Set<Prod
@Nonnull
private CompletableFuture<Void> createAndUpdateProducts() {
final CompletableFuture<Void> createRequestsStage =
productService.createProducts(draftsToCreate)
.thenAccept(createdProducts -> updateStatistics(createdProducts, draftsToCreate.size()))
.toCompletableFuture();
createProducts(draftsToCreate)
.thenAccept(createdProducts -> updateStatistics(createdProducts, draftsToCreate.size()))
.toCompletableFuture();

final CompletableFuture<List<Optional<Product>>> updateRequestsStage =
syncProducts(productsToSync).toCompletableFuture();

return allOf(createRequestsStage, updateRequestsStage);
}

@Nonnull
private CompletionStage<Set<Product>> createProducts(@Nonnull final Set<ProductDraft> productsDrafts) {
return mapValuesToFutureOfCompletedValues(productsDrafts, this::applyCallbackAndCreate)
.thenApply(results -> results.filter(Optional::isPresent).map(Optional::get))
.thenApply(createdProducts -> createdProducts.collect(Collectors.toSet()));
}

@Nonnull
private CompletionStage<Optional<Product>> applyCallbackAndCreate(@Nonnull final ProductDraft productDraft) {
return syncOptions
.applyBeforeCreateCallBack(productDraft)
.map(productService::createProduct)
.orElse(CompletableFuture.completedFuture(Optional.empty()));
}

private void updateStatistics(@Nonnull final Set<Product> createdProducts,
final int totalNumberOfDraftsToCreate) {
final int numberOfFailedCreations = totalNumberOfDraftsToCreate - createdProducts.size();
Expand Down
11 changes: 0 additions & 11 deletions src/main/java/com/commercetools/sync/services/CategoryService.java
Expand Up @@ -49,17 +49,6 @@ public interface CategoryService {
@Nonnull
CompletionStage<Optional<Category>> fetchCategory(@Nullable final String key);

/**
* Given a {@link Set} of categoryDrafts, this method creates Categories corresponding to them in the CTP project
* defined in a potentially injected {@link io.sphere.sdk.client.SphereClient}.
*
* @param categoryDrafts set of categoryDrafts to create on the CTP project.
* @return {@link CompletionStage}&lt;{@link Map}&gt; in which the result of it's completion contains a {@link Set}
* of all created categories.
*/
@Nonnull
CompletionStage<Set<Category>> createCategories(@Nonnull final Set<CategoryDraft> categoryDrafts);

/**
* Given a {@code key}, this method first checks if cached map of category keys -&gt; ids is not empty.
* If not, it returns a completed future that contains an optional that contains what this key maps to in
Expand Down
11 changes: 0 additions & 11 deletions src/main/java/com/commercetools/sync/services/ProductService.java
Expand Up @@ -72,17 +72,6 @@ public interface ProductService {
@Nonnull
CompletionStage<Optional<Product>> fetchProduct(@Nullable final String key);

/**
* Given a {@link Set} of productsDrafts, this method creates Products corresponding to them in the CTP project
* defined in a potentially injected {@link io.sphere.sdk.client.SphereClient}.
*
* @param productsDrafts set of productsDrafts to create on the CTP project.
* @return {@link CompletionStage}&lt;{@link Map}&gt; in which the result of it's completion contains a {@link Set}
* of all created products.
*/
@Nonnull
CompletionStage<Set<Product>> createProducts(@Nonnull final Set<ProductDraft> productsDrafts);

/**
* Given a {@link ProductDraft}, this method creates a {@link Product} based on it in the CTP project defined in
* a potentially injected {@link io.sphere.sdk.client.SphereClient}. The created product's id and key are also
Expand Down

0 comments on commit 9cc4b57

Please sign in to comment.