Showing posts with label Replication. Show all posts
Showing posts with label Replication. Show all posts

January 4, 2021
Estimated Post Reading Time ~

AEM Automation – Deploy & Replicate Package

Guide:

Following is the Powershell script that will automate the process of uploading and install the AEM package to Author Instance. The uploaded package will be replicated to publish instances using cURL. Following are the parameters,

param( 
[String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CQ_USER="admin", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CQ_PASS="admin", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$AEMPORT="4502", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$HOSTNAME="localhost", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$PKGNAME="aemdev.ui.apps-1.0-SNAPSHOT.zip", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$PKGPATH="C:\Users\SKYDEVOPS\Documents\AEMMVN\aemdev\ui.apps\target\${PKGNAME}", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$GCURL="http://${HOSTNAME}:${AEMPORT}/crx/packmgr/service.jsp", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$RCURL="http://${HOSTNAME}:${AEMPORT}/crx/packmgr/service/script.html/etc/packages/aemdev/${PKGNAME}?cmd=replicate", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CURLEXE="C:\tools\curl761\bin\curl.exe" ) 

Following is the cURL command that will Install-Package on AEM instance, which needs username and password to authenticate package installation, also a path to the package that needs to be installed

Write-Output "Installing APPS Package" cmd.exe /c $CURLEXE -u "${CQ_USER}:${CQ_PASS}" -F file=@"$PKGPATH" -F name="AEMDEV" -F force=true -F install=true "$GCURL" | Out-Null Write-Output "Package Installation Complete"

Following is the cURL command that will replicate the installed package to all the publishers

Write-Output "Replicating Package" cmd.exe /c $CURLEXE -u "${CQ_USER}:${CQ_PASS}" -X POST $RCURL Write-Output "Replication Complete"

Following is the entire script

# Parameters param( [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CQ_USER="admin", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CQ_PASS="admin", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$AEMPORT="4502", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$HOSTNAME="localhost", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$PKGNAME="aemdev.ui.apps-1.0-SNAPSHOT.zip", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$PKGPATH="C:\Users\SKYDEVOPS\Documents\AEMMVN\aemdev\ui.apps\target\${PKGNAME}", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$GCURL="http://${HOSTNAME}:${AEMPORT}/crx/packmgr/service.jsp", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$RCURL="http://${HOSTNAME}:${AEMPORT}/crx/packmgr/service/script.html/etc/packages/aemdev/${PKGNAME}?cmd=replicate", [String][parameter(Mandatory=$False,ValueFromPipeline=$False)]$CURLEXE="C:\tools\curl761\bin\curl.exe" 

# Installing Packages 
Write-Output "Installing APPS Package" 
cmd.exe /c $CURLEXE -u "${CQ_USER}:${CQ_PASS}" -F file=@"$PKGPATH" -F name="AEMDEV" -F force=true -F install=true "$GCURL" | Out-Null 
Write-Output "Package Installation Complete" 

# Replicating package 
Write-Output "Replicating Package" 
cmd.exe /c $CURLEXE -u "${CQ_USER}:${CQ_PASS}" -X POST $RCURL 
Write-Output "Replication Complete"


By aem4beginner

January 2, 2021
Estimated Post Reading Time ~

How to Transform Replication URLs in AEM

If you are ever faced with the requirement of replicating AEM content between systems where the content hierarchy on one of the systems does not exactly match the other system, this post is for you.

Let’s take this example:
Your AEM author instance has pages under “/content/site/page/”

and you have a sling mapping that transforms incoming URLs:

“/content/site/page/subpage/<pageName>” to “/content/site/page/<pageName>”

For example, your sling mapping transforms the URL: “/content/site/page/subpage/mypage” to “/content/site/page/mypage”

in this case the dispatcher will create cache in this directory: “/content/site/page/subpage/mypage”. Now you try to publish “/content/site/page/mypage” but the dispatcher cache is not flushed, why? because the replication path is actally “/content/site/page/mypage” (not the same as the cached path). The replication agent only knows your author/publisher hiarchy.

To solve this issue, we need to transform the replication path, we can achieve this by implementing the ReplicationPathTransformer

ReplicationPathTransformer was introduced in AEM 6.2

public interface ReplicationPathTransformer

Transforms the replication path for the replication request. It provides a hook, while building and importing replication content package, that can map a given replication path (i.e. /content/dam/skiing/images/Banner.JPG) to new target replication path (i.e. /content/dam/campaigns/skiing/images/Banner.JPG) This can be used when you need to replicate between system where path hierarchies on one system do not exactly match to path hierarchies on other system. Please note that it only allows for one-to-one mapping between source and target for a replication request. Implementations can choose using #accepts(javax.jcr.Session, ReplicationAction, Agent), whether to map/transform given replication path in the context of the current replication request.

Here is an example ReplicationPathTransformer implementation you can use to start your implementation:

package org.kp.fdl.facility.replication;
import com.day.cq.replication.Agent;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationLog;
import com.day.cq.replication.ReplicationPathTransformer;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Session;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.List;
@Component(metatype=false, immediate=true)
@Service({ReplicationPathTransformer.class})
public class ExampleReplicationPathTransformer implements ReplicationPathTransformer
{
@Override
public String transform(Session session, String replicationPath, ReplicationAction replicationAction, Agent agent)
{
/*
* transform the replicationPath
*/
// for the sake of simplicity, and as this is an example return the replicationPath as is.
return replicationPath
}
@Override
public boolean accepts(Session session, ReplicationAction replicationAction, Agent agent)
{
/* Check if the agent is a dispatcher agent
* if it is the agent you are targeting, return true
*/
// transform all urls
return true;
}
}



By aem4beginner

December 28, 2020
Estimated Post Reading Time ~

5 Popular Ways to Replicate a Page in AEM

In AEM 6+ there are many ways to accomplish the goal to “replicate” a page. This article will display 5 Popular Ways to Replicate a Page in AEM. At the end of this article, you should be able to “replicate” a page using different methods within AEM.

Typically a scaled AEM structure, security is the highest priority. Typically Content Author’s privileges are restricted, so AEM page publishing is restricted; therefore AEM Workflows are utilized. 

1. Touch UI – Site Console – Quick Publish
If you have the right privilege to replicate a page, and all its referenced resources, you can simply publish a page with the “Quick Publish” button from the Site Console’s navigation.

How to publish a page using the “Quick Publish” AEM feature:
Step 1: Navigate to the Touch UI – Sites Console http://localhost:4502/sites.html/content.
Step 2: Select on the “Targeted Page” by clicking on the thumbnail tile.
Step 3: Click on “Quick Publish” to replicate the Page.
Done.



2. Touch UI – Site Console – Manage Publication
If you have the right privilege to replicate a page, and all its referenced resources, you can simply replicate a page with the “Quick Publish” button from the Site Console’s navigation.

But if you wish to manage the “referenced resources”, you can use the “Manage Publication” replication AEM feature.

How to publish a page using the Manage Publication AEM feature:
Step 1: Navigate to the Touch UI – Sites Console http://localhost:4502/sites.html/content.
Step 2: Select on the “Targeted Page” by clicking on the thumbnail tile.
Step 3: Click on the “Manage Publication” (if your screen is too small, click on the drop-down … icon to expose this link).
Step 4: Select the “Publish” option.
Step 5: Click on “Next”.
Step 6: Review References: By default, all resources in AEM that are referenced by the targeted page will be replicated. References items should be reviewed before replicating the page.
Step 7: Review References – Select on the “Targeted Page” by clicking on the thumbnail tile.
Step 8: Review References – Click on “Published References” to reveal a dialogue of all the referenced resources.
Step 9: Review References – Manage “Targeted References”; radio button clicked mean to “include resource as a part of the page replication. Click on “Done” to save settings.
Step 10: Click on “Publish” to replicate the Page.
Done.

Steps 2 & 3
 

Steps 4 & 5
 

Steps 6…10


3. Touch UI – Editable Page – Sidebar Drop Options
If you have the right privilege to replicate a page, after a change within the AEM Page Editor, replication from the sidebar drop options can be utilized to replicate the page.
How to publish a page using the “Editable Page” AEM feature:
Step 1: Navigate to the Touch UI – Sites Console http://localhost:4502/editor.html/content/we-retail/us/en/about-us.html.
Step 2: Click on “Publish Page” to replicate the Page.



4. Touch UI – Activate a Page with an AEM Workflow
Typically a scaled AEM structure, security is the highest priority. Typically Content Author’s privileges are restricted, so AEM page publishing is restricted; therefore AEM Workflows are utilized.

One of the most popular page publishing type AEM Workflows utilized in AEM is Content Approval AEM Workflows. Content Approval AEM Workflows are implemented based on the business requirements and context of the AEM customer, but as an example of this blog article, we will take the “Request to Publish”, out of box AEM Workflow model as an example.

The “Request to Publish” is an example of an out of the box AEM Workflow model that can be used by Content Authors with less privileges. When the “Request to Publish” AEM Workflow has been started, an AEM Workflow task is sent to a supporting user-group, for them to approve the request. Once the request has been approved, the page will be replicated; typically a system user is replicating the page with the correct privileges.

How to publish a page using the “Activate a Page” AEM Workflow:
Step 1: Navigate to the Touch UI – Sites Console http://localhost:4502/sites.html/content.
Step 2: Select on the “Targeted Page” by clicking on the thumbnail tile.
Step 3: Click on “Create” to reveal a drop down menu & Click on “Workflow”.
Step 4: Select on the targeted “Workflow Model”.
Step 5: Input a “Workflow Title”.
Step 6: Click on “Next”.
Step 7: Click on “Create”.
Done. When the page is approved the page will be published.

Steps 2 & 3
 

Steps 4…6
 

Step 7


5. CRX/DE – Replication Tab
In some cases where an administrator needs to quickly publish a page, they are utilizing the “Replication Tab” within the CRX/DE web console. This feature is only used by users who know what they are doing.

How to publish a page using the “CRX/DE Replication Tab”:
Step 1: Navigate to the CRX/DE Console http://localhost:4502/crx/de/index.jsp
.
Step 2: Select on the “Targeted Page”.
Step 3: Select the “Replication” tab.
Step 4: Click on “Replicate” to replicate the Page.
Done.




By aem4beginner

October 13, 2020
Estimated Post Reading Time ~

Using Replicator in AEM

This article covers the basic usage of replicator in AEM where we sometimes can’t replicate the bulk nodes in AEM or we want to create a replicator which can replicate a specific node in our AEM code.

I have created the object of the Replictor using Reference annotation in my Java servlet:


import com.day.cq.replication.Replicator;

import com.day.cq.replication.Replicator;

@Reference
Replicator replicator;

@Reference
private transient ResourceResolverFactory resourceResolverFactory;



Now get the resourceResolver from the service user and get the Session out of it.

Map<String, Object> serviceUserAuth= Collections.singletonMap(ResourceResolverFactory.SUBSERVICE,
"serviceUser");
ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(serviceUserAuth);

Session session = resourceResolver.adaptTo(Session.class);


Once you get the session you can use the below code snippet to replicate the node that you have in your code:

replicator.replicate(session,ReplicationActionType.ACTIVATE,node.getPath());


By aem4beginner

September 22, 2020
Estimated Post Reading Time ~

Pause option in AEM replication agent queue

At some point, we need to stop the replication from AEM author to the publisher for some time or a few minutes, so that no content gets published from author to publisher instance. May we need to handle the maintenance window or stop accepting new content from authors. So the question here is how we can do this without disabling the AEM replication queue. 

How to pause the replication agent queue?

Out of the box, the AEM replication agent queue has an option "pause", using that pause option we could stop the replication. When you will pause the queue then all content which will get published by authors during that time frame will be enlisted in the queue but those will not get processed. The pause option will only pause your queue but your replication queue will be still enabled and active.

Active AEM replication queue:Active AEM replication queue

Paused AEM replication queue:

Paused AEM replication queue

Points to remember:

  • When we will restart our instance then this paused status will be overridden and we will find the queue is active.
  • By default, it will pause for an hour.

Hope this helps you to understand the pause option and its role in the AEM replication queue.

References:

  1. Troubleshoot AEM replication queue



By aem4beginner

September 19, 2020
Estimated Post Reading Time ~

Error during replication of ReplicationAction | AEM to Demandware

Error during content replication from Adobe experience manager(AEM) to Demandware. If you are seeing this error logged in AEM error.log file or demandware replication agent log, then it means credential configured in configuration Demandware TransportHandler Plugin for WebDAV to transport the data and content from AEM to Demandware is not valid.

*ERROR* [sling-threadpool-a430ddf1-339b-4140-b796-18d2fc7b5a66-(apache-sling-job-thread-pool)-7-com_day_cq_replication_job_demandware(com/day/cq/replication/job/demandware)] com.day.cq.replication.Agent.demandware Error during replication of ReplicationAction{type=ACTIVATE, path[0]='/content/dam/website/content/january/sample_image_winter.jpg', time=1578013615072, userId='rashidjorvee@jorvee.com', revision='null'}: com.day.cq.replication.ReplicationException: com.github.sardine.impl.SardineException: Unexpected response (401 )

03.01.2020 01:07:56.111 *INFO* [sling-threadpool-a430ddf1-339b-4140-b796-18d2fc7b5a66-(apache-sling-job-thread-pool)-7-com_day_cq_replication_job_demandware(com/day/cq/replication/job/demandware)] com.day.cq.replication.Agent.demandware.queue Job for agent demandware processed in 281ms. Failed.

com.day.cq.replication.ReplicationException: com.github.sardine.impl.SardineException: Unexpected response (401 )

Solution: 
This error you seeing because of bad WebDev credential. Check the configured WebDev login credential under AEM config manager (http://localhost:4502/system/console/configMgr) in configuration Demandware TransportHandler Plugin for WebDAV.



Demandware TransportHandler Plugin for WebDAV

If you think your credential in valid then please try login directly on the Demanware instance URL with configured credential to verify the credential is valid and working.

Reference: 


By aem4beginner

May 12, 2020
Estimated Post Reading Time ~

Applying “Binary less” Replication

When configuring replication agents, one of the options for the serialization type setting is “Binary less”.

We will see what a “Binary less” replication mean, how it behaves, the use cases where its applicable and an approach that can be used for configuring it in this blog.

What is Binary less replication?
Binary less replication means that the binaries are left out from the content being replicated. When replicating an asset for example, only the metadata of the asset gets replicated. The binaries of the asset which comprises of the original asset and all its renditions are not included in the replicated content.

When can it be used?
Binary less replication is useful when multiple AEM instances are sharing a common datastore. The binaries are shared through the common datastore and hence there is no need to replicate them to all the instances.

How it works?
It’s important to understand the way binary less replication works to properly configure it for your scenario.

While creating a replication package for the content being replicated, the binaries are replaced with their hash code reference. The package with hash code references for the binaries is then sent to the receiver. When the receiver installs the received replication package, it tries to resolve this hash code reference to the binary in its datastore.

Since the receiver shares the same datastore as that of the sender, it resolves the hash code reference to the actual binary in the datastore and links it where the hash code references are included in the replicated content.

If the datastore is not configured properly or for some reason, the receiver is not able to resolve the binary based on the hash code reference, it falls back to the default replication mode and redoes the replication including the binaries in the replication package.

The overall replication does not fail in this case. The status if the binary less replication was successful can be checked in the logs.

Check for log statements with text patterns “FAILED PATHS START”, “FAILED PATHS END” for details on failed binary less replications and text pattern “set using a reference” for successful binary less replication.

Use cases of Binary less replication
Binary less replication is useful in setups using shared datastore across instances. One common use case is when all AEM instances uses a common datastore (author and all publishers), the replication agents from author to all the publishers can be configured with binary less serialization type as the asset upload to the author would place the binaries in the datastore - which is shared by all the publish instances as well.

Special cases - Approach for configuring Binary less replication
Replication configuration should be carefully thought-out when we have setups where all instances do not share the same datastore, but instead distinct groups of servers have shared datastore.

Some common setup configurations where this applies are

A shared datastore for all publishers, but a separate data store for author
Separate shared datastore for Primary and DR environments

Let us take the case 1 where the author has a separate datastore and all publish instances share a common datastore.

This configuration is illustrated below

In this case, configuring binary less for replication to all publish instances from author would cause the replication to fail and fallback to default replication in an ad-hoc manner. The asynchronous nature of replication to all publishers simultaneously causes the no. of failed binary less replication to be high.

To overcome this situation, its ideal to designate one publish instance as a Gateway instance in such scenarios. Configure author to perform default replication to this gateway publish instance. This step would make sure that the binary gets replicated to the single publish instance and gets persisted in its datastore (which is also shared by other publish instances)

Now configure the gateway instance to chain replicate the content to the other publisher instances in binary less mode. Chain replication starts after the successful install of the content on the gateway instance. This ensures that the binaries are replicated and persisted in the shared datastore through the gateway instance before the binary less replication kicks in for replicating content to other instances in the cluster.

This configuration is illustrated in the below diagram

In cases where the setup includes a DR environment with a separate shared datastore for publish instances with-in the DR, the same configuration can be replicated by designating one instance in DR as gateway.

The author in this case should be configured to perform default replication to the gateway instances of both the Primary and DR cluster. The gateway instances can then chain replicate the content to other instances within its cluster.

Limitations with this approach:
Using this approach discussed in the above section to leverage binary less replication through gateway instances does come with a few limitations that we need to be aware of and plan for

Introduces delay in replication completion
It introduces delay in the replication completion. Also during the interval between replication to gateway and the completion of replication, the content between the gateway instance and other instances within a cluster would be out of sync.

Usually this gets completed in few seconds but could go higher depending on the load on the system and the no. of concurrent replications performed.

In cases where content sync mismatch for even such short duration is not acceptable, the gateway could be removed out of the instances serving the content to the end users. This way only the other publish instances which have their content in near real-time sync would be serving content for the end users.

Replication status reflected on author:
Note that with this approach, the status turns green (stating replication successful) as soon as the replication to the gateway instance is successful. This could send a wrong signal to the content publishers, especially when there is delay or issues encountered in chain replication.

Gateway instance failure
Another aspect that must be planned for with the above configuration is in the event of gateway instance failures.

When a gateway instance fails for some reason, another instance within the cluster should be promoted as the new gateway with replication agents on the author and the new gateway reconfigured to have a working setup. Be ready with scripts to reconfigure the instances which can be run in the event of a gateway failure.


By aem4beginner

Replication under the hood – What happens when you click activate?

When we click the activate button, we know that the item activated gets replicated to all the publish instances that have an active replication agent configured.

The status of the replication is reflected as yellow icon while the replication is in progress which subsequently turns green or red depending on the final replication status.

But what happens internally during this process?

A sequence of steps happens on the sender side before an item gets placed in the replication queue of each applicable replication agent after which the content gets transferred to the receiver where it gets processed to complete the replication.

At a high level, the following are the steps that are performed
  1. Version creation
  2. Activation process on sender and placement of queue item
  3. Transfer of replicated content to receiver
  4. Processing replication on receiver
  5. Status update and retry if applicable
The high level flow is depicted in the below diagram


Version creation
The first step performed in activation is the creation of a frozen version of the content being replicated. This ensures that subsequent edits to the content while the replication is in progress do not impact the content being replicated.

The frozen version thus created gets attached to the replication process and it’s this frozen content that would get replicated.

Activation process on sender
When the activation process kicks in, a sequence of steps happens and results in an item getting placed in the replication queue of each associated agents. These steps are



Configuration Collation
One or more replication agents could be configured as active depending on the no. of publish instances to replicate to. The configuration of these replication agents could defer to the extent that the content to be replicated for a given activation could be different. For this reason, the replication package is created separately for each of the replication agent configured.

The first step in the activation process on the sender, is the identification of all the active replication agents that are configured. For each of the agent identified, its configuration is collated and kept as a ReplicationOptions object.

Permission check
Then a check is performed to validate if the user performing the activate action has the required permission to replicate the content that are selected for activation. Only the nodes that the activating user has replication permission on gets included for replication.

Preprocessing
After collating the replication agent configuration and performing the permission check, all the preprocessors (optional, if there are any), gets applied.

A custom preprocessor can be implemented by creating a class that implements the Preprocessor interface. Providing an implementation for its preprocess method in this class and configuring it as a service will register it as a preprocessor.

All the preprocessors that are thus configured gets called at this stage before proceeding to the next step.

Replication package creation
After applying all the preprocessors, the next step is the creation of the replication package for the activated item.

A different replication package gets created for each replication agent depending on its configuration (based on the ReplicationOptions object created in the previous steps) and using the serializer as per the serialization type configured.

The replication package contains all the information needed for replication process to complete on the receiver end.

Queuing (Persist in JCR)
The created replication package gets persisted in JCR along with other metadata information like the item on which the activation is performed, type of action, user id, and so on. It gets stored in JCR under the node /var/eventing/jobs.

Once the replication package gets persisted in JCR, the activation process steps on the sender side is complete and is ready for transport over the network on to the receiver.

Also at this stage, the item is visible in the queue associated with the replication agent. The items in the queue are shown by querying for pending items under /var/eventing/jobs for the queue id associated with that replication agent.

Transfer to receiver
The responsibility of transferring the replication package over the network lies with the sender AEM instance. The sling job monitors for items that becomes available for replication and kicks off the process to transfer it to the receiver.

The sling job for a queue is synchronous. It processes the first item in the queue and only when its complete, it picks up the next item for processing.

Processing on receiver
The listener on the receiving end on receiving a replication package, performs deserialization of the received package and installs it to get the content replicated on to itself.

A success status is sent back if all the steps are successful on the receiver side. If any of the step fails, a failure status is returned prompting the sender to retry

Status update and retry
After transferring the replication package, the sling job on the sender waits for the response from the receiver indicating the status of the replication at the receiving end.

If the response is successful, it removes the item from the queue (deletes the item from JCR), marking the status of replication as successful.

In case the replication is not successful on the receiving end, the item on the queue is retained for reprocessing. The sling job then waits for the ‘Retry Delay’ duration to elapse before retrying to send the item again to the receiver.


By aem4beginner

Poison messages on Replication Queues

So what does it take for a replication queue to get blocked. Well… Just one bad item on the queue.

Yes. One item that has issue would completely block the queue.

This is because of the way the items in the queue gets processed. The items in the queue are ordered and are processed strictly in the first-in-first-out (FIFO) order.

This order needs to be preserved to make sure that there are no overlapping writes of the content and the data integrity is preserved.

So what happens when a single item could not be processed?

Simply it will not get removed from the replication queue and will continue to remain at the head of the queue. Meaning it will continue to remain as the next item to be processed. When the retry happens, this item again fails thus continuing to remain at the head of the queue forever.

Unless this item gets processed and removed from the queue, the next items will not get a chance to get processed

This can be checked by looking at the queue item details in JCR. Look under the path /var/eventing/jobs/assigned in JCR to locate the queue item.

The first item in a blocked queue would have undergone multiple retries



While all the subsequent items would have retry count as 0, indicating the processing has not happened for it


By aem4beginner

Why an item fails to get replicated?

There are many reasons why replication of an item fails and the replication queue gets built up. Some of the common reasons to check for are
  1. Agent configuration
  2. Network issue
  3. Permissions issue
  4. Missing namespaces / node types
  5. Oak conflicts
Agent configuration
The reason for replication not working could simply be because the agent configuration is wrong.

If you are doing the configuration for the first time or making any changes to the configuration, make sure to perform ‘Test connection’ check to make sure that there is no inadvertent error in the configuration.

Network issue
Replication failures are often caused by network issues. It could be a temporary glitch in which case the queue items gets cleared once the network is restored.

A java.net.ConnectException in the logs of the sender AEM instance is an indication of network issue preventing replication from being successful.

You could see messages like

Error while sending request: java.net.ConnectException: Connection refused: connect

On the sender side error.log file.

Permissions issue
Replication could fail if the transport user does not have write permission for the replicated content on the target instance.

The issue occurs when the replication process installs the replicated content on the receiver end. You would see error message indicating ‘Access denied’ in the logs.

Error message would read as

com.day.cq.replication.ReplicationException: Repository error during node import: Access denied.


On the sender and receiver side error.log file.

Missing namespaces / node types
The custom namespaces and node types that are created must be created on the publish side as well.

Typically, the namespaces and node types creation should be included in the code package so that it gets installed on all instances.

If it gets missed out and if the item replicated used custom node types or name spaces, replication could fail.

This can be detected by looking for “Invalid namespace prefix” and “Invalid node type” messages in the error.log

Oak conflicts
Replication might fail due to conflicts when writing content to the oak repository.
This is more likely to happen on instances configured with Mongo repository when more than on instance shares a common repository.

Look for messages with “Unresolved conflicts” to identify issues due to oak conflicts

Closing Note
The above are only some of the main reasons for replication failure. It is not an exhaustive list of all the reasons for replication failures.

For any replication issue, perform these three basic checks
  1. Perform test connection to make sure connectivity is not an issue
  2. Check the error.log on the sender side. This would almost always give you the cause of the issue
  3. Check the error.log on the receiver side for more details on the cause of the issue.


By aem4beginner

How to configure Dispatcher Flush Agents on Publisher?

Normally the dispatcher flush agents are configured on the author under the section ‘Agents on Publish’ and activated for it to be replicated to publish instances where it takes effect. This works when the same configuration is needed on all the publish instances as any node that gets replicated would get replicated to all publishers for which a replication agent is configured

For cases where the configuration of dispatcher flush agent(s) needed on each publish instance differs we can use one of the below approach

Direct configuration on Publish instances
Make the configuration directly on Publish instance. This involves logging in to each publish instance with admin credentials and creating the required dispatcher flush agent configuration for that instance. Avoid using this approach for higher environments. Could be a useful approach for development and test environment to get the configuration done quickly.

Using CURL scripts to create dispatcher flush agents
Use CURL to create dispatcher flush agents needed on each publish instance. This is the most widely used approach and the CURL scripts can be maintained for recreating instances in case of server rebuilds.

Using packages to install the configuration on publish instances
Configure all the dispatcher flush agents needed on Author. Create packages – one for each publish instance with flush agents needed for that instance. Install the created package on the corresponding publish instance.


By aem4beginner

May 10, 2020
Estimated Post Reading Time ~

Custom Transport Handler in CQ5.6/AEM6

There are a lot of blogs written on this one. However, none of them explain about how it should be unit tested and what are the point a programmer should have in mind while using a custom transport handler. I am going to list down few of them below.

1. There must be a custom ContentBuilder defined. An example definition could be as follows.

@Component(metatype = false)
@Service(ContentBuilder.class)
@Property(name = "name", value = "Custom Content Builder")

public class CustomContentBuilder implements ContentBuilder { public static final String NAME = "Custom Content Builder"; public static final String TITLE = "Custom Content Builder";
public ReplicationContent create(Session session, ReplicationAction action,
ReplicationContentFactory factory) throws ReplicationException { return ReplicationContent.VOID;
}

public String getName() { return TITLE;
}

public String getTitle() { return TITLE;
}

public ReplicationContent create(Session session, ReplicationAction action,
ReplicationContentFactory factory, Map<String, Object> arg3){


// TODO Auto-generated method stub
return ReplicationContent.VOID;
}
}

2. While defining your agent in author agents that uses your custom transport handler, select your custom content builder's name i.e. in above case it is "Custom Content Builder" as a value of Synchronization Type.



In above example instead of default it should be "Custom Content Builder"

Notes: The custom handler can be invoked on the author environment by invoking activate request on the page. This works fine even when the custom handler is configured on the publish side and the request is invoked from author side in case of AEM 6.0 but, this will not work fine in CQ5.6


By aem4beginner

Replications in AEM

Replication agents are central to Adobe Experience Manager (AEM) as the mechanism used to:
  • Publish (activate) content from an author to a publish environment.
  • Explicitly flush content from the Dispatcher cache.
  • Return user input (for example, form input) from the publish environment to the author environment (under control of the author environment).
Requests are queued to the appropriate agent for processing.
User data (users, user groups, and user profiles) are not replicated between author and publish instances.
For multiple publish instances, user data is Sling distributed when User Synchronisation is enabled.

Replicating from Author to Publish
Replication, to a publish instance or dispatcher, takes place in several steps:
the author requests that certain content be published (activated); this can be initiated by a manual request, or by automatic triggers which have been preconfigured. 

The request is passed to the appropriate default replication agent; an environment can have several default agents which will always be selected for such actions.

The replication agent “packages” the content and places it in the replication queue.
in the Websites tab the colored status indicator is set for the individual pages.
the content is lifted from the queue and transported to the publish environment using the configured protocol; usually this is HTTP.

A servlet in the publish environment receives the request and publishes the received content; the default servlet is http://localhost:4503/bin/receive.
multiple author and publish environments can be configured.


Replicating from Publish to Author

Some features allow users to enter data on a publish instance.
In some cases, a type of replication known as reverse replication, is needed to return this data to the author environment from where it is redistributed to other publish environments. Due to security considerations, any traffic from the publish to the author environment must be strictly controlled.
Reverse replication uses an agent in the publish environment which references the author environment. This agent places the data into an outbox. This outbox is matched with replication listeners in the author environment. The listeners poll the outboxes to collect any data entered and then distribute it as necessary. This ensures that the author environment controls all traffic.

In other cases, such as for Communities features (for example, forums, blogs, comments, and reviews), the amount of user generated content (UGC) being entered in the publish environment is difficult to efficiently synchronize across AEM instances using replication.
AEM Communities never uses replication for UGC. Instead, the deployment for Communities requires a common store for UGC.

Replication – Out of the Box

To follow this example and use the default replication agents you need to Install AEM with:
the author environment on port 4502
the publish environment on port 4503

Enabled by default :
Agents on author : Default Agent (publish)
Effectively disabled by default (as of AEM 6.1) :
Agents on author : Reverse Replication Agent (publish_reverse)
Agents on publish : Reverse Replication (outbox)

Replication Agents – Out of the Box

The following agents are available in a standard AEM installation:
Default Agent
Used for replicating from author to publish.
Dispatcher Flush
This is used for managing the Dispatcher cache.
Reverse Replication
Used for replicating from publish to author. Reverse replication is not used for Communities features, such as forums, blogs, and comments. It is effectively disabled as the outbox is not enabled. Use of reverse replication would require custom configuration.
Static Agent
This is an “Agent that stores a static representation of a node into the filesystem.”.
For example with the default settings, content pages and dam assets are stored under /tmp, either as HTML or the appropriate asset format.
This was requested so that when the page is requested directly from the application server the content can be seen. This is a specialized agent and (probably) will not be required for most instances.

Replication Agents – Configuration Parameters

When configuring a replication agent from the Tools console, four tabs are available within the dialog:

Settings
Name : A unique name for the replication agent.
Description : A description of the purpose this replication agent will serve.
Enabled : Indicates whether the replication agent is currently enabled.
When the agent is enabled the queue will be shown as:
Active when items are being processed.
Idle when the queue is empty.
Blocked when items are in the queue, but cannot be processed; for example, when the receiving queue is disabled.

Serialisation Type : The type of serializations:
Default: Set if the agent is to be automatically selected.
Dispatcher Flush: Select this if the agent is to be used for flushing the dispatcher cache.

Retry Delay : The delay (waiting time in milliseconds) between two retries, should a problem be encountered.
Default: 60000
Agent User Id :
Depending on the environment, the agent will use this user account to:
collect and package the content from the author environment
create and write the content on the publish environment
Leave this field empty to use the system user account (the account defined in sling as the administrator user; by default this is admin).

Caution:
For an agent on the author environment this account must have read access to all paths that you want to have replicated.

For an agent on the publish environment this account must have the create/write access required to replicate the content.

Note:
This can be used as a mechanism for selecting specific content for replication.

Log Level : Specifies the level of detail to be used for log messages.
Error: only errors will be logged
Info: errors, warnings and other informational messages will be logged
Debug: a high level of detail will be used in the messages, primarily for debug purposes
Default: Info

Use for reverse replication : Indicates whether this agent will be used for reverse replication; returns user input from the publish to author environment.

Alias update : Selecting this option enables alias or vanity path invalidation requests to Dispatcher.
Transport
URI
This specifies the receiving servlet at the target location. In particular, you can specify the hostname (or alias) and context path to the target instance here.
For example:
A Default Agent may replicate to http://localhost:4503/bin/receive
A Dispatcher Flush agent may replicate to http://localhost:8000/dispatcher/invalidate.cache
The protocol specified here (HTTP or HTTPS) will determine the transport method.
For Dispatcher Flush agents, the URI property is used only if you use path-based virtualhost entries to differentiate between farms, you use this field to target the farm to invalidate. For example, farm #1 has a virtual host of http://www.mysite.com/path1/* and farm #2 has a virtual host of http://www.mysite.com/path2/*. You can use a URL of /path1/invalidate.cache to target the first farm and /path2/invalidate.cache to target the second farm.

User
User name of the account to be used for accessing the target.

Password
Password for the account to be used for accessing the target.

NTLM Domain
Domain for NTML authentication.

NTLM Host
Host for NTML authentication.

Enable relaxed SSL
Enable if you want self-certified SSL certificates to be accepted.

Allow expired certs
Enable if you want expired SSL certificates to be accepted.

Proxy
The following settings are only needed if a proxy is needed:
Proxy Host
Hostname of the proxy used for transport.

Proxy Port
Port of the proxy.

Proxy User
User name of the account to be used.

Proxy Password
Password of the account to be used.

Proxy NTLM Domain
The proxy NTLM domain.

Proxy NTLM Host
The proxy NTLM domain.

Extended
Interface

Here you can define the socket interface to bind to.
This sets the local address to be used when creating connections. If this is not set, the default address will be used. This is useful for specifying the interface to use on multi-homed or clustered systems.

HTTP Method
The HTTP method to be used.
For a Dispatcher Flush agent this is nearly always GET and should not be changed (POST would be another possible value).

HTTP Headers
These are used for Dispatcher Flush agents and specify elements that must be flushed.
For a Dispatcher Flush agent the three standard entries should not need changing:
CQ-Action:{action}
CQ-Handle:{path}
CQ-Path:{path}
These are used, as appropriate, to indicate the action to be used when flushing the handle or path. The sub-parameters are dynamic:
{action} indicates a replication action
{path} indicates a path
They are substituted by the path/action relevant to the request and therefore do not need to be “hardcoded”:

Note:
If you have installed AEM in a context other than the recommended default context, then you will need to register the context in the HTTP Headers. For example:
CQ-Handle:/<yourContext>{path}

Close Connection
Enable to close the connection after each request.

Connect Timeout
Timeout (in milliseconds) to be applied when trying to establish a connection.

Socket Timeout
Timeout (in milliseconds) to be applied when waiting for traffic after a connection has been established.

Protocol Version
Version of the protocol; for example 1.0 for HTTP/1.0.
Triggers
These settings are used to define triggers for automated replication:
Ignore default
If checked, the agent is excluded from default replication; this means it will not be used if a content author issues a replication action.

On Modification
Here a replication by this agent will be automatically triggered when a page is modified. This is mainly used for Dispatcher Flush agents, but also for reverse replication.

On Distribute
If checked, the agent will automatically replicate any content that is marked for distribution when it is modified.

On-/Offtime reached
This will trigger automatic replication (to activate or deactivate a page as appropriate) when the ontimes or offtimes defined for a page occur. This is primarily used for Dispatcher Flush agents.

On Receive
If checked, the agent will chain replicate whenever receiving replication events.

No Status Update
When checked the agent will not force a replication status update.

No Versioning
When checked the agent will not force versioning of activated pages.

Configuring your Replication Agents
Controlling Access to Replication Agents
Access to the pages used to configure the replication agents can be controlled by using user and/or group page permissions on the etc/replication node.

Note:
Setting such permissions will not affect users replicating content (e.g. from the Websites console or sidekick option). The replication framework does not use the “user session” of the current user to access replication agents when replicating pages.

Caution:
Do not use the “Test Connection” link for the Reverse Replication Outbox on a publish instance.
If a replication test is performed for an Outbox queue, any items that are older than the test replication will be re-processed with every reverse replication.
If such items already exist in a queue, they can be found with the following XPath JCR query and should be removed.
/jcr:root/var/replication/outbox//*[@cq:repActionType=’TEST’]

How do I use reverse replication and what’s necessary to make sure that it works?
Out-of-the box, only cq:Page nodes are reverse replicated. For any other node, it’s necessary to use the two last methods, as a project-specific implementation.
There are three possibilities
Use the SlingPostServlet (that is, do not create any custom post servlets or POST.jsp to handle the incoming requests) so that it implicitly triggers a related PageEvent. Then set a property name “cq:distribute” and set its value to “true” on the nodes you want to reverse replicate.
To implement this solution, it’s unnecessary to write any code. You can use the Form component to set all the necessary hidden fields.
Use your own code that accesses the repository, modify the properties “cq:lastModified,” “cq:lastModifiedBy” and “cq:distribute.”
Posted data can be controlled, internal code writes the data.
To implement this solution, it’s necessary to write the code for your project.

Use your own code that calls the replicate method from Replicator service with options to use distribution mode.
Replication is controlled from your code.
To implement this solution, write the code specific for your project.

Use your own code to implement a reverse replication solution
Add the following code to fire the event related to the page you want to reverse replicate (the example below was extracted from sample PostDataServlet.java):

 ... 
// set the page to hide in the navigation Node pageContainer = newCommentPage.getContentResource().adaptTo(Node.class); pageContainer.setProperty("cq:lastModified", Calendar.getInstance()); pageContainer.setProperty("cq:lastModifiedBy", session.getUserID()); pageContainer.setProperty("cq:distribute", true); ... session.save(); ...

Attached is an example using a component to render the form and display the previous post. For each post, it creates a subpage that contains a paragraph with text in it. By doing so, it ensures that each post can be managed separately (and avoids collision with posts that could be generated from other publish instances). The storage location is defined as a parameter in the component dialog (that is, /content/usergenerated/comments/form1, which you can create using a folder in the siteadmin).

On the author instance, you can define a workflow model that would be launched when a page is created below your comments page. Make sure that you clear the cq:distribute value in your workflow, if you reactivate the content on author to the publish, otherwise it goes in an endless loop !!!

On the publish instance, make sure that the user has sufficient rights to create content. If you test with anonymous, then change the rights accordingly using CRX Explorer for the given jcr path).

Note on replication
For replication to work properly then store data with the following rules:
(1) the replicated (root) node’s nodetype must extend nt:hierarchyNode
(2) all direct child nodes that are not nt:hierarchyNodes must be aggregated
(3) the subtrees of all nodes from (2), apart from nodetypes, must be aggregated
Adobe recommends to use the cq:Page (/jcr:content) as container for your data, as you can then easily manage it and use it with the user interface (siteadmin, and so on). You can use PageManager API to create the page.

Note:
Certain terms related to publishing can be confused:
Publish / Unpublish
These are the primary terms for the actions that make your content publicly available on your publish environment (or not).
Activate / Deactivate
These terms are synonymous with publish/unpublish.
Replicate / Replication
These are the technical terms describing the movement of data (e.g. page content, files, code, user comments) from one environment to another such as when publishing or reverse-replicating user comments.

Note:
If you do not have the required privileges for publishing a specific page:
  • A workflow will be triggered to notify the appropriate person of your request to publish.
  • This workflow may have been customized by your development team.
  • A message will be displayed briefly to notify you that the workflow was triggered.
Depending on your location, you can publish:
  • From the page editor
  • From the sites console
From Page Editor
Depending on whether the page has references that need publishing:
The page will be published directly if there are no references to be published.
If the page has references that need publishing, these will be listed in the Publish wizard, where you can either:
  • Specify which of the assets/tags/etc. you want to publish together with the page, then use Publish to complete the process.
  • Use Cancel to abort the action.
Note:
Publishing from the editor is a shallow publish, i.e. only the selected page/pages is/are published and any child pages are not.

From Sites Console
In the sites console there are two options for publishing:
  • Quick Publish
  • Manage Publication
Quick Publish
Quick Publish is for simple cases and publishes the selected page(s) immediately without any further interaction. Because of this, any non-published references will also be published automatically.
Note:
Quick Publish is a shallow publish, i.e. only the selected page/pages is/are published and any child pages are not.

Manage Publication
Manage Publication offers more options than Quick Publish, allowing for the inclusion of child pages, customization of the references, and starting any applicable workflows as well as offering the option to publish at a later date.


By aem4beginner

May 4, 2020
Estimated Post Reading Time ~

Ways to Replicate a Node

Our first entry discusses how to replicate a node from AEM.

Adobe Experience Manager is a very deep product. If you don’t live and breathe it on a daily basis, you might forget how to perform certain tasks, or maybe you are new to AEM altogether. One task that we have found people struggling with from time to time is how to replicate a node that isn’t a traditional “cq:Page”. 

There are two simple methods that we have used to replicate arbitrary nodes that cannot be replicated using the Touch UI. The first is found under /etc/replication.html, the other is found in the CRXDE Lite.

On /etc/replication.html, find the “Activate Tree” link, which will allow you to specify a path and replicate it:



In CRXDE Lite, use the button on the “Replication” tab:




By aem4beginner

May 3, 2020
Estimated Post Reading Time ~

Creating a custom Akamai replication agent in AEM

Replication is central to the AEM experience. AEM is about content and replication is how you move that content across servers. You're most likely using at least two of the out-of-the-box replication agents provided by Adobe: your default agent activates content from an author to publish and your dispatcher flush agent clears your Dispatcher cache. AEM provides several other replication agents for tasks such as replicating in reverse (publish to the author), moving content within the Adobe Marketing Cloud products such as Scene 7 and Test and Target, and static agents for replicating to the file system. This blog post details the steps used to create your own custom replication agents.


The following sample project demonstrates a custom replication agent that purges Akamai CDN (Content Delivery Network) cached content. The full code for this blog is hosted on GitHub. There are three pieces to this project: the transport handler, the content builder, and the replication agent's user interface.

Transport Handler
TransportHandler implementations control the communication with the destination server and determine when to report back a positive ReplicationResult to complete the activation and when to report back a negative ReplicationResult returning the activation to the queue.
AkamaiTransportHandler.java

package com.nateyolles.aem.core.replication;

import java.io.IOException;
import java.util.Arrays;

import com.day.cq.replication.AgentConfig;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationLog;
import com.day.cq.replication.ReplicationResult;
import com.day.cq.replication.ReplicationTransaction;
import com.day.cq.replication.TransportContext;
import com.day.cq.replication.TransportHandler;

import org.apache.commons.codec.CharEncoding;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.jackrabbit.util.Base64;

/**
 * Transport handler to send test and purge requests to Akamai and handle
 * responses. The handler sets up basic authentication with the user/pass from
 * the replication agent's transport config and sends a GET request as a test
 * and POST as purge request. A valid test response is 200 while a valid purge
 * response is 201.
 * 
 * The transport handler is triggered by setting your replication agent's
 * transport URL's protocol to "akamai://".
 *
 * The transport handler builds the POST request body in accordance with
 * Akamai's CCU REST APIs {@link https://api.ccu.akamai.com/ccu/v2/docs/}
 * using the replication agent properties. 
 */
@Service(TransportHandler.class)
@Component(label = "Akamai Purge Agent", immediate = true)
public class AkamaiTransportHandler implements TransportHandler {

    /** Protocol for replication agent transport URI that triggers this transport handler. */
    private final static String AKAMAI_PROTOCOL = "akamai://";

    /** Akamai CCU REST API URL */
    private final static String AKAMAI_CCU_REST_API_URL = "https://api.ccu.akamai.com/ccu/v2/queues/default";

    /** Replication agent type property name. Valid values are "arl" and "cpcode". */
    private final static String PROPERTY_TYPE = "akamaiType";

    /** Replication agent multifield CP Code property name.*/
    private final static String PROPERTY_CP_CODES = "akamaiCPCodes";

    /** Replication agent domain property name. Valid values are "staging" and "production". */
    private final static String PROPERTY_DOMAIN = "akamaiDomain";

    /** Replication agent action property name. Valid values are "remove" and "invalidate". */
    private final static String PROPERTY_ACTION = "akamaiAction";

    /** Replication agent default type value */
    private final static String PROPERTY_TYPE_DEFAULT = "arl";

    /** Replication agent default domain value */
    private final static String PROPERTY_DOMAIN_DEFAULT = "production";

    /** Replication agent default action value */
    private final static String PROPERTY_ACTION_DEFAULT = "remove";

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canHandle(AgentConfig config) {
        final String transportURI = config.getTransportURI();

        return (transportURI != null) ? transportURI.toLowerCase().startsWith(AKAMAI_PROTOCOL) : false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ReplicationResult deliver(TransportContext ctx, ReplicationTransaction tx)
            throws ReplicationException {

        final ReplicationActionType replicationType = tx.getAction().getType();

        if (replicationType == ReplicationActionType.TEST) {
            return doTest(ctx, tx);
        } else if (replicationType == ReplicationActionType.ACTIVATE ||
                replicationType == ReplicationActionType.DEACTIVATE) {
            return doActivate(ctx, tx);
        } else {
            throw new ReplicationException("Replication action type " + replicationType + " not supported.");
        }
    }

    /**
     * Send test request to Akamai via a GET request.
     *
     * Akamai will respond with a 200 HTTP status code if the request was
     * successfully submitted. The response will have information about the
     * queue length, but we're simply interested in the fact that the request
     * was authenticated.
     *
     * @param ctx Transport Context
     * @param tx Replication Transaction
     * @return ReplicationResult OK if 200 response from Akamai
     * @throws ReplicationException
     */
    private ReplicationResult doTest(TransportContext ctx, ReplicationTransaction tx)
            throws ReplicationException {

        final ReplicationLog log = tx.getLog();
        final HttpGet request = new HttpGet(AKAMAI_CCU_REST_API_URL);
        final HttpResponse response = sendRequest(request, ctx, tx);

        if (response != null) {
            final int statusCode = response.getStatusLine().getStatusCode();

            log.info(response.toString());
            log.info("---------------------------------------");

            if (statusCode == HttpStatus.SC_OK) {
                return ReplicationResult.OK;
            }
        }

        return new ReplicationResult(false, 0, "Replication test failed");
    }

    /**
     * Send purge request to Akamai via a POST request
     *
     * Akamai will respond with a 201 HTTP status code if the purge request was
     * successfully submitted.
     *
     * @param ctx Transport Context
     * @param tx Replication Transaction
     * @return ReplicationResult OK if 201 response from Akamai
     * @throws ReplicationException
     */
    private ReplicationResult doActivate(TransportContext ctx, ReplicationTransaction tx)
            throws ReplicationException {

        final ReplicationLog log = tx.getLog();
        final HttpPost request = new HttpPost(AKAMAI_CCU_REST_API_URL);

        createPostBody(request, ctx, tx);

        final HttpResponse response = sendRequest(request, ctx, tx);

        if (response != null) {       
            final int statusCode = response.getStatusLine().getStatusCode();

            log.info(response.toString());
            log.info("---------------------------------------");
            
            if (statusCode == HttpStatus.SC_CREATED) {
                return ReplicationResult.OK;
            }
        }

        return new ReplicationResult(false, 0, "Replication failed");
    }

    /**
     * Build preemptive basic authentication headers and send request.
     *
     * @param request The request to send to Akamai
     * @param ctx The TransportContext containing the username and password
     * @return HttpResponse The HTTP response from Akamai
     * @throws ReplicationException if a request could not be sent
     */
    private <T extends HttpRequestBase> HttpResponse sendRequest(final T request,
            final TransportContext ctx, final ReplicationTransaction tx)
            throws ReplicationException {

        final ReplicationLog log = tx.getLog();
        final String auth = ctx.getConfig().getTransportUser() + ":" + ctx.getConfig().getTransportPassword();
        final String encodedAuth = Base64.encode(auth);
        
        request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + encodedAuth);
        request.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());

        HttpClient client = HttpClientBuilder.create().build();
        HttpResponse response;

        try {
            response = client.execute(request);
        } catch (IOException e) {
            throw new ReplicationException("Could not send replication request.", e);
        }

        return response;
    }

    /**
     * Build the Akamai purge request body based on the replication agent
     * settings and append it to the POST request.
     *
     * @param request The HTTP POST request to append the request body
     * @param ctx TransportContext
     * @param tx ReplicationTransaction
     * @throws ReplicationException if errors building the request body 
     */
    private void createPostBody(final HttpPost request, final TransportContext ctx,
            final ReplicationTransaction tx) throws ReplicationException {

        final ValueMap properties = ctx.getConfig().getProperties();
        final String type = PropertiesUtil.toString(properties.get(PROPERTY_TYPE), PROPERTY_TYPE_DEFAULT);
        final String domain = PropertiesUtil.toString(properties.get(PROPERTY_DOMAIN), PROPERTY_DOMAIN_DEFAULT);
        final String action = PropertiesUtil.toString(properties.get(PROPERTY_ACTION), PROPERTY_ACTION_DEFAULT);

        JSONObject json = new JSONObject();
        JSONArray purgeObjects = null;

        /*
         * Get list of CP codes or ARLs/URLs depending on agent setting
         */
        if (type.equals(PROPERTY_TYPE_DEFAULT)) {

            /*
             * Get the content created with the custom content builder class
             * 
             * The list of activated resources (e.g.: ["/content/geometrixx/en/blog"])
             * is available in tx.getAction().getPaths(). For this example, we want the
             * content created in our custom content builder which is available in
             * tx.getContent().getInputStream().
             */
            try {
                final String content = IOUtils.toString(tx.getContent().getInputStream());

                if (StringUtils.isNotBlank(content)) {
                    purgeObjects = new JSONArray(content);
                }
            } catch (IOException | JSONException e) {
                throw new ReplicationException("Could not retrieve content from content builder", e);
            }
        } else {
            final String[] cpCodes = PropertiesUtil.toStringArray(properties.get(PROPERTY_CP_CODES));
            purgeObjects = new JSONArray(Arrays.asList(cpCodes));
        }

        if (purgeObjects != null && purgeObjects.length() > 0) {
            try {
                json.put("type", type)
                    .put("action", action)
                    .put("domain", domain)
                    .put("objects", purgeObjects);
            } catch (JSONException e) {
                throw new ReplicationException("Could not build purge request content", e);
            }

            final StringEntity entity = new StringEntity(json.toString(), CharEncoding.ISO_8859_1);
            request.setEntity(entity);

        } else {
            throw new ReplicationException("No CP codes or pages to purge");
        }
    }
}

The default HTTP transport handler sends an authenticated request to the destination server specified in the "Transport URI" setting and expects an HTTP response with a status code of "200 OK" in order to mark the activation as successful. The Akamai CCU REST API responds with a status code of "201 Created" for a successful purge request. Therefore, a custom Transport Handler was utilized and given the responsibility for sending the POST requests, looking for 201 responses, and returning the proper ReplicationResult.

The transport handler service determines which transport handler to use based on the overridden canHandle method. You'll notice that any replication agent configured with a "Transport URI" that begins with http:// or https:// will be handled by AEM's HTTP transport handler. The convention is to create a unique URL protocol/scheme and have your transport handler's canHandle method watch for Transport URIs that start with your URL scheme. For example, by navigating to a clean instance's Agents on Author page, you'll find default AEM replication agents using http://, static://, tnt://, s7delivery:// and repo://. In this example, the transport handler is activated on the amakai:// scheme and uses the hardcoded Akamai API REST endpoint. A popular convention is to set your transport handler's Transport URIs that start with something like "foo-", which allows the user to configure their replication agent with either "foo-http://" or "foo-https://" and have your transport handler simply remove the custom prefix before making the HTTP request.


The transport handler also handles other ReplicationActionTypes. The Akamai example implements the standard "Test Connection" feature of replication agents by making a GET request to the Akamai API endpoint which expects a "200 OK" response in return - it's simply testing the replication agent's configured username and password. The example does not implement other replication action types such as deletions, deactivations, and polling (reverse).

When you speak of a replication agent, you're first thought will probably be of the default agent that moves content from an author to publish via HTTP. Likewise, I've been discussing the Akamai transport handler example and its usage of HTTP GET and POST requests. However, it's important to note that your transport handler doesn't need to make HTTP calls. For example, you can write an FTP transport handler or a transport handler that interacts with the server's file system like the static replication agent does. Your transport handler can do anything as long as it returns a positive or negative replication result to update the queue.

Content Builder
ContentBuilder implementations build the body of the replication request. Implementations of the ContentBuilder interface end up as serialization options in the replication agent configuration dialog alongside the Default, Dispatcher Flush, Binary less, and Static Content Builder options.
AkamaiContentBuilder.java

package com.nateyolles.aem.core.replication;

import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.Path;

import javax.jcr.Session;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.jcr.resource.JcrResourceConstants;

import com.day.cq.commons.Externalizer;
import com.day.cq.replication.ContentBuilder;
import com.day.cq.replication.ReplicationAction;
import com.day.cq.replication.ReplicationContent;
import com.day.cq.replication.ReplicationContentFactory;
import com.day.cq.replication.ReplicationException;
import com.day.cq.replication.ReplicationLog;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;

/**
 * Akamai content builder to create replication content containing a JSON array
 * of URLs for Akamai to purge through the Akamai Transport Handler. This class
 * takes the internal resource path and converts it to external URLs as well as
 * adding vanity URLs and pages that may Sling include the activated resource.
 */
@Component(metatype=false)
@Service(ContentBuilder.class)
@Property(name="name", value="akamai")
public class AkamaiContentBuilder implements ContentBuilder {

    @Reference
    private ResourceResolverFactory resolverFactory;

    /** The name of the replication agent */
    public static final String NAME = "akamai";

    /**
     * The serialization type as it will display in the replication
     * agent edit dialog selection field.
     */
    public static final String TITLE = "Akamai Purge Agent";

    /**
     * {@inheritDoc}
     */
    @Override
    public ReplicationContent create(Session session, ReplicationAction action,
            ReplicationContentFactory factory) throws ReplicationException {
        return create(session, action, factory, null);
    }

    /**
     * Create the replication content containing the public facing URLs for
     * Akamai to purge.
     */
    @Override
    public ReplicationContent create(Session session, ReplicationAction action,
            ReplicationContentFactory factory, Map<String, Object> parameters)
            throws ReplicationException {

        final String path = action.getPath();
        final ReplicationLog log = action.getLog();

        ResourceResolver resolver = null;
        PageManager pageManager = null;
        JSONArray jsonArray = new JSONArray();

        if (StringUtils.isNotBlank(path)) {
            try {
                HashMap<String, Object> sessionMap = new HashMap<>();
                sessionMap.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
                resolver = resolverFactory.getResourceResolver(sessionMap);

                if (resolver != null) {
                    pageManager = resolver.adaptTo(PageManager.class);
                }
            } catch (LoginException e) {
                log.error("Could not retrieve Page Manager", e);
            }

            if (pageManager != null) {
                Page purgedPage = pageManager.getPage(path);

                /*
                 * Get the external URL if the resource is a page. Otherwise, use the
                 * provided resource path.
                 */
                if (purgedPage != null) {
                    /* 
                     * Use the Externalizer, Sling mappings, Resource Resolver mapping and/or
                     * string manipulation to transform "/content/my-site/foo/bar" into
                     * "https://www.my-site.com/foo/bar.html". This example assumes a custom
                     * "production" externalizer setting.
                     */
                    Externalizer externalizer = resolver.adaptTo(Externalizer.class);
                    final String link = externalizer.externalLink(resolver, "production", path) + ".html";

                    jsonArray.put(link);
                    log.info("Page link added: " + link);

                    /*
                     * Add page's vanity URL if it exists.
                     */
                    final String vanityUrl = purgedPage.getVanityUrl();

                    if (StringUtils.isNotBlank(vanityUrl)) {
                        jsonArray.put(vanityUrl);
                        log.info("Vanity URL added: " + vanityUrl);
                    }

                    /*
                     * Get containing pages that includes the resource.
                     */
                    // Run project specific query

                } else {
                    jsonArray.put(path);
                    log.info("Resource path added: " + path);
                }

                return createContent(factory, jsonArray);
            }
        }

        return ReplicationContent.VOID;
    }

    /**
     * Create the replication content containing 
     *
     * @param factory Factory to create replication content
     * @param jsonArray JSON array of URLS to include in replication content
     * @return replication content
     *
     * @throws ReplicationException if an error occurs
     */
    private ReplicationContent createContent(final ReplicationContentFactory factory,
            final JSONArray jsonArray) throws ReplicationException {

        Path tempFile;

        try {
            tempFile = Files.createTempFile("akamai_purge_agent", ".tmp");
        } catch (IOException e) {
            throw new ReplicationException("Could not create temporary file", e);
        }

        try (BufferedWriter writer = Files.newBufferedWriter(tempFile, Charset.forName("UTF-8"))) {
            writer.write(jsonArray.toString());
            writer.flush();

            return factory.create("text/plain", tempFile.toFile(), true);
        } catch (IOException e) {
            throw new ReplicationException("Could not write to temporary file", e);
        }
    }

    /**
     * {@inheritDoc}
     *
     * @return {@value #NAME}
     */
    @Override
    public String getName() {
        return NAME;
    }

    /**
     * {@inheritDoc}
     *
     * @return {@value #TITLE}
     */
    @Override
    public String getTitle() {
        return TITLE;
    }
}

The provided Akamai example project could have been done without implementing a content builder; the logic in the content builder could have been completed in the transport handler as the transport handler created it's own request anyways. Another thing to consider is whether you need the session as ContentBuilder implementations give you that while TransportHandler implementations do not.

A perfect example of utilizing the content builder is in Andrew Khoury's Dispatcher refetching flush agent where the default HTTP transport handler is still used to communicate with the Dispatcher and only the HTTP request body needed to be built out in order for Dispatcher to fetch and re-cache content.

User Interface
By implementing a content builder, the user can simply use a default replication agent and choose the custom serialization type. However, the Akamai replication agent requires the following custom configurations:
  1. an option to remove versus invalidate content
  2. an option to purge resources versus purging via CP Codes
  3. an option to purge the production versus staging domain
  4. the reverse replication option removed

A clean user interface was provided in order for users to implement and configure the Akamai replication agent. To accomplish this, a custom cq:Template as well as a corresponding cq:Component including the view and dialog was made. The easiest way is to copy the default replication agent from /libs/cq/replication/templates/agent and /libs/cq/replication/components/agent to /apps/your-project/replication and update the agent like any other AEM component.

To keep things clean and simple, the Akamai replication agent component inherits from the default replication agent by setting the sling:resourceSuperType to cq/replication/components/agent. The only update needed to the copied component was the dialog options and the agent.jsp file as it contains JavaScript to open the dialog for which you need to update the path. Any additions to the dialog can be retrieved through the TransportContext's getConfig().getProperties() ValueMap.

Download the Akamai Replication Agent project from GitHub.



By aem4beginner