Acorel
To Acorel.nl

Integrating SAP Commerce Cloud with Bynder DAM

Daniël Roest, 29 May 2019

You’ll probably know that SAP Commerce Cloud has its own Digital Asset Management (DAM) module to store and maintain digital media like product pictures, videos, manuals, technical documents and marketing materials so you can use them throughout your online platform. But being an open platform, it’s also possible to integrate with other DAM solutions. In this blog I will show you how we integrated SAP Commerce Cloud with Bynder, one of world’s leading DAM solutions.

The integration we built is bidirectional: we are able to add new digital assets in Bynder and synchronize them to SAP Hybris Commerce and vice versa. And the same is true for its properties and relations to products. By doing this we support different scenarios for our internal marketing department, but also for our suppliers when they maintain their products and related media themselves.

Let’s focus on how to synchronize media from Bynder to SAP Commerce Cloud and use the above asset in Bynder as an example. It consists of an image of a shower and on the right, you’ll see some properties linked to it such as the type and subtype of the image, ETIM and GS1 classification system attributes and the product IDs the asset is linked to.

We decided to synchronize new and changed media in Bynder via a periodical CronJob that pulls the changes from Bynder and stores them in SAP Commerce Cloud. Bynder provides an SDK that we can use to call its APIs.

The CronJob we created calls syncBynderProductMedias() in class DefaultBynderMediaService which does the following.

[javascript]
@Override
public void syncBynderProductMedias(Date lastSyncDate, Integer maxMediaToFetch, Boolean overwriteProductPicture) {
try {
Stopwatch stopWatch = Stopwatch.createStarted();
sessionService.removeAttribute(BYNDER_PROCESSING_PRODUCTS);

log.info("Fetch changed media after [{}]", lastSyncDate);
Optional<PagedMedia> medias = newBynderClient().getPagedMedia(
null,
lastSyncDate,
1
);

medias.ifPresent(
media -> media.getTotal().ifPresent(total -> {
int pages = BigDecimal.valueOf(total.getCount())
.divide(BigDecimal.valueOf(MAX_MEDIA_PER_PAGE), BigDecimal.ROUND_UP)
.intValue();

log.info("Total media found to be processed: [{}]", total.getCount());
log.info("Process {} media items – page (1/{})", media.getMedias().size(), pages);

// process media items found in 1st page
processMedias(lastSyncDate, media.getMedias(), BooleanUtils.toBoolean(overwriteProductPicture));

// process media items of next pages
if (total.getCount() > MAX_MEDIA_PER_PAGE) {
AtomicInteger page = new AtomicInteger(1);

do {
Optional<PagedMedia> pagedMedia = newBynderClient().getPagedMedia(
null,
lastSyncDate,
page.incrementAndGet()
);
pagedMedia.ifPresent(m -> {
log.info("Process {} media items – page ({}/{})", m.getMedias().size(), page.get(), pages);
processMedias(lastSyncDate, m.getMedias(), BooleanUtils.toBoolean(overwriteProductPicture));
});
} while (pages > page.get());
}
})
);

stopWatch.stop();
log.info("Bynder media job synchronisation completed and took {}.", stopWatch);
} finally {
sessionService.removeAttribute(BYNDER_PROCESSING_PRODUCTS);
}
}
[/javascript]

It starts with calling the getPagedMedia() method of the BynderClient class with the date the last synchronization took place. getPagedMedia() creates a request that contains our query using this date and posts it to the Bynder API endpoint. Bynder responses with a JSON containing the assets and their properties that were added or changed since that date.

[javascript]
public Optional<PagedMedia> getPagedMedia(String propertyOptionId, Date dateModified, int page) {
OAuthRequest request = new OAuthRequest(Verb.GET, getApiHost() + GET_MEDIA_URL);

if (propertyOptionId != null) {
addQueryStringParameters(request, "propertyOptionId", propertyOptionId);
}
addQueryStringParameters(request, "dateModified", dateModified != null ? new SimpleDateFormat("yyyyMMdd").format(dateModified) : null);
addQueryStringParameters(request, "limit", String.valueOf(MAX_MEDIA_PER_PAGE));
addQueryStringParameters(request, "page", String.valueOf(page));
addQueryStringParameters(request, "total", String.valueOf(1));

log.debug(request.getCompleteUrl());

getOAuthService().signRequest(getAccessToken(), request);
Response response = request.send();

if (response.isSuccessful()) {
Gson gson = new GsonBuilder().create();
return Optional.ofNullable(gson.fromJson(response.getBody(), PagedMedia.class));
} else {
if (log.isErrorEnabled()) {
log.error("Response not successful: {} {}\nRequest: {}",
Integer.toString(response.getCode()),
response.getMessage(),
request.getCompleteUrl());
}
}

return Optional.empty();
}
[/javascript]

Back in syncBynderProductMedias(), via processMedias() we call processMedia() that does the actual processing for each Bynder asset.

[javascript]
private void processMedia(Media media, CatalogVersionModel mediaOnlineCatalogVersion, CatalogModel masterCatalog, boolean overwriteProductPicture) {
// find or create bynder media
BynderMediaModel bynderMediaModel = findOrCreateAndSaveBynderMedia(media, mediaOnlineCatalogVersion);

// update properties if different from Bynder
updatePropertiesFromBynder(media, bynderMediaModel);

// update media container with the new groups (catalogs) if different
updateMediaContainerForGroups(media, bynderMediaModel);

Collection<String> productCodes = toSafeCollection(media.getProductCodes());

// unlink all other products with a different code in bynder media
findLinkedProductsNotInProductCodes(bynderMediaModel, productCodes)
.forEach(product -> unlinkMediaFromProduct(bynderMediaModel, product));

Set<CatalogModel> mediaCatalogs = getCatalogsFromMedia(media);

if (!mediaCatalogs.isEmpty()) {
// unlink all products with different catalog then media group
unlinkProductsWithDifferentCatalog(media, bynderMediaModel, mediaCatalogs, masterCatalog);

// link media to products
linkMediaToProducts(productCodes, bynderMediaModel, mediaCatalogs, masterCatalog, overwriteProductPicture);
} else {
log.warn("Media has no group. No linking or un-linking will happen!");
}
}
[/javascript]

We store the Bynder assets in SAP Commerce Cloud as a BynderMedia item which is a subtype of the standard Media item type. The processMedia() method uses the unique Bynder ID from the Bynder media to search for it. If it can be found it will update the existing instance of BynderMediaModel or otherwise it will create a new one.

Second, it updates the properties of the asset with updatePropertiesFromBynder(). And finally, it reads one of its properties that contains all the product IDs the Bynder media is linked to, to either link or unlink the Bynder media to one or more products in the product catalog.

After all this processing, the asset is stored as a BynderMedia in SAP Commerce Cloud.

And it’s linked to one or more products in the product catalog.

As said earlier it’s also possible to synchronize changes in the opposite direction: from SAP Commerce Cloud to Bynder. By using a PrepareInterceptor on BynderMedia we can trigger an update on the media and its basic properties. But also, when products are linked or unlinked to Bynder media, we are able to trigger an update. We update certain properties such as the product IDs, but also several properties based on the classification of the product. This ensures that also in Bynder the assets are classified with the correct attribute values. Let’s discuss this in more detail in one of our future blogs.

Daniël Roest

Read all my blogs

Receive our weekly blog by email?
Subscribe here: