Akamai's REST API supports a number of calls. In this article I will only cover the calls to:
- Make a Purge Request
- Check a Purge Status
I've implemented an AkamaiRestFlusher in the form of a Deployer Extension. When a page in SDL Web (Tridion) is sent for publishing, this extension triggers the Akamai purge automatically.
A properties file named publications-domain.properties provides the list of public website base domains known by Akamai keyed by publication id. For example:
91=http://www.firstexample.com
93=http://www.secondexample.com
96=http://www.thirdexample.com
The deployer extension takes the data available from the deployment package, like the publication id and page URL Path, to construct the page URL known by Akamai.
In addition to constructing the page URL for purging, the extension also constructs a list of binary URLs referenced by the page. The purge request will include the page URL and all binary URLs in one call.
The AwaitPurgeCompletion property of the deployer extension allows the administrator to determine whether the publishing task should hold and wait until the entire purge completes before continuing. In case this is set to true, the extension utilizes the pingAfterSeconds attribute, available in the response from the purge call, to halt the processing before submitting a call to check the purge status. The extension continues submitting calls to check the purge status until the purge completion is confirmed or the maximum waiting time of 30 minutes is reached.
Akamai purges usually take time to complete, so the general advise is to set the AwaitPurgeCompletion property to false.
All communication is performed using JSON.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tridion.ps.akamai; | |
import org.codehaus.jackson.annotate.JsonIgnoreProperties; | |
@JsonIgnoreProperties(ignoreUnknown = true) | |
public class AkamaiPurgeResponse { | |
private Integer httpStatus; | |
private Integer estimatedSeconds; | |
private Integer pingAfterSeconds; | |
private String detail; | |
private String purgeId; | |
private String progressUri; | |
private String supportId; | |
public Integer getHttpStatus() { | |
return httpStatus; | |
} | |
public void setHttpStatus(Integer httpStatus) { | |
this.httpStatus = httpStatus; | |
} | |
public String getDetail() { | |
return detail; | |
} | |
public void setDetail(String detail) { | |
this.detail = detail; | |
} | |
public Integer getEstimatedSeconds() { | |
return estimatedSeconds; | |
} | |
public void setEstimatedSeconds(Integer estimatedSeconds) { | |
this.estimatedSeconds = estimatedSeconds; | |
} | |
public String getPurgeId() { | |
return purgeId; | |
} | |
public void setPurgeId(String purgeId) { | |
this.purgeId = purgeId; | |
} | |
public String getProgressUri() { | |
return progressUri; | |
} | |
public void setProgressUri(String progressUri) { | |
this.progressUri = progressUri; | |
} | |
public Integer getPingAfterSeconds() { | |
return pingAfterSeconds; | |
} | |
public void setPingAfterSeconds(Integer pingAfterSeconds) { | |
this.pingAfterSeconds = pingAfterSeconds; | |
} | |
public String getSupportId() { | |
return supportId; | |
} | |
public void setSupportId(String supportId) { | |
this.supportId = supportId; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tridion.ps.akamai; | |
import java.io.File; | |
import java.io.FileReader; | |
import java.io.IOException; | |
import java.io.UnsupportedEncodingException; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Properties; | |
import org.apache.http.auth.AuthScope; | |
import org.apache.http.auth.UsernamePasswordCredentials; | |
import org.apache.http.client.ClientProtocolException; | |
import org.apache.http.client.CredentialsProvider; | |
import org.apache.http.client.HttpClient; | |
import org.apache.http.client.methods.HttpGet; | |
import org.apache.http.client.methods.HttpPost; | |
import org.apache.http.entity.StringEntity; | |
import org.apache.http.HttpEntity; | |
import org.apache.http.HttpResponse; | |
import org.apache.http.impl.client.BasicCredentialsProvider; | |
import org.apache.http.impl.client.HttpClientBuilder; | |
import org.apache.http.impl.client.HttpClients; | |
import org.apache.http.message.BasicHeader; | |
import org.codehaus.jackson.JsonParseException; | |
import org.codehaus.jackson.map.JsonMappingException; | |
import org.codehaus.jackson.map.ObjectMapper; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import com.tridion.configuration.*; | |
import com.tridion.deployer.ProcessingException; | |
import com.tridion.deployer.Processor; | |
import com.tridion.deployer.Module; | |
import com.tridion.transport.transportpackage.*; | |
@SuppressWarnings({"deprecation", "unused"}) | |
public class AkamaiRestFlusher extends Module { | |
private static Logger log = LoggerFactory.getLogger(AkamaiRestFlusher.class); | |
private String akamaiUsername = null; | |
private String akamaiPassword = null; | |
private String akamaiDomain = null; | |
private String awaitPurgeCompletion = null; | |
public AkamaiRestFlusher(Configuration config, Processor processor) throws ConfigurationException { | |
super(config, processor); | |
} | |
// This method is called once for each TransportPackage that is deployed | |
public void process(TransportPackage data) throws ProcessingException { | |
log.info("Entering AkamaiFlusher"); | |
try { | |
akamaiUsername = config.getChild("AkamaiUsername").getContent(); | |
akamaiPassword = config.getChild("AkamaiPassword").getContent(); | |
akamaiDomain = config.getChild("AkamaiDomain").getContent(); | |
awaitPurgeCompletion = config.getChild("AwaitPurgeCompletion").getContent(); | |
} catch(Exception e) { | |
log.error("Could not get the Akamai username and password. No Akamai flushing.", e); | |
return; | |
} | |
if ((akamaiUsername == null || akamaiUsername.isEmpty()) || (akamaiPassword == null || akamaiPassword.isEmpty())) { | |
log.error("Akamai user account credentials not provided. No Akamai flushing."); | |
return; | |
} | |
String domainValue = null; | |
try { | |
int publicationId = data.getProcessorInstructions().getPublicationId().getItemId(); | |
String domainsPropsFilePath = config.getChild("WebsiteDomains").getContent(); | |
Properties domainProperties = new Properties(); | |
domainProperties.load(new FileReader(new File(domainsPropsFilePath))); | |
domainValue = domainProperties.getProperty(String.valueOf(publicationId)); | |
} catch (Exception e) { | |
log.error("Could not get domains for publication. No Akamai flushing.", e); | |
return; | |
} | |
if (domainValue == null || domainValue.isEmpty()) { | |
log.error("No domain available for publication. No Akamai flushing"); | |
return; | |
} | |
List<String> urls = new ArrayList<String>(); | |
String[] domains = domainValue.split(","); | |
PageMetaData pageFile = (PageMetaData)data.getMetaDataFile("Pages"); | |
if (pageFile != null) { | |
List<Page> pages = pageFile.getPages(); | |
for ( Page page : pages ) { | |
for (String domain : domains) { | |
urls.add(domain + page.getURLPath()); | |
} | |
} | |
} | |
BinaryMetaData binaryFile = (BinaryMetaData)data.getMetaDataFile("Binaries"); | |
if (binaryFile != null) { | |
List<Binary> binaries = binaryFile.getBinaries(); | |
for ( Binary binary : binaries ) { | |
String binaryType = binary.getType(); | |
if (binaryType == null || !binaryType.startsWith("image/")) { | |
for (String domain : domains) { | |
urls.add(domain + binary.getURLPath()); | |
} | |
} | |
} | |
} | |
try { | |
// Now talk to Akamai | |
purgeUrls(urls); | |
} catch(Exception e) { | |
log.error("Error purging URLs [" + e.getMessage() + "]"); | |
throw new ProcessingException("Error purging URLs [" + e.getMessage() + "]"); | |
} | |
log.info("Completed AkamaiFlusher"); | |
log.info("===================================="); | |
} | |
private void purgeUrls(List<String> urls) throws ProcessingException { | |
try { | |
String domain = "production"; | |
if (akamaiDomain != null && akamaiDomain.equalsIgnoreCase("Staging")) { | |
domain = "staging"; | |
} | |
Boolean awaitAkamai = Boolean.TRUE; | |
if (awaitPurgeCompletion != null && awaitPurgeCompletion.equalsIgnoreCase("False")) { | |
awaitAkamai = Boolean.FALSE; | |
} | |
StringBuilder strBuilder = new StringBuilder(); | |
strBuilder.append("{"); | |
strBuilder.append("\"action\" : \"invalidate\" ,"); | |
strBuilder.append("\"domain\" : \"" + domain + "\" ,"); | |
strBuilder.append("\"objects\": ["); | |
for ( int i=0; i<urls.size(); i++ ) { | |
String url = urls.get(i); | |
strBuilder.append("\"" + url + "\""); | |
if ( i+1<urls.size() ) { | |
strBuilder.append(","); | |
} | |
} | |
strBuilder.append("]"); | |
strBuilder.append("}"); | |
CredentialsProvider provider = new BasicCredentialsProvider(); | |
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(akamaiUsername, akamaiPassword); | |
provider.setCredentials(AuthScope.ANY, credentials); | |
HttpClient httpClient = HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build(); | |
HttpPost postRequest = new HttpPost("https://api.ccu.akamai.com/ccu/v2/queues/default"); | |
postRequest.setHeader(new BasicHeader("Content-Type", "application/json")); | |
postRequest.setHeader(new BasicHeader("Accept", "application/json")); | |
postRequest.setEntity(new StringEntity(strBuilder.toString())); | |
HttpResponse response = httpClient.execute(postRequest); | |
HttpEntity entity = response.getEntity(); | |
ObjectMapper mapper = new ObjectMapper(); | |
AkamaiPurgeResponse akamaiPurgeResp = mapper.readValue(entity.getContent(), AkamaiPurgeResponse.class); | |
log.info("--------------------------"); | |
log.info("JSON object: " + strBuilder.toString()); | |
log.info("httpStatus: " + akamaiPurgeResp.getHttpStatus()); | |
log.info("detail: " + akamaiPurgeResp.getDetail()); | |
log.info("estimatedSeconds: " + akamaiPurgeResp.getEstimatedSeconds()); | |
log.info("purgeId: " + akamaiPurgeResp.getPurgeId()); | |
log.info("progressUri: " + akamaiPurgeResp.getProgressUri()); | |
log.info("pingAfterSeconds: " + akamaiPurgeResp.getPingAfterSeconds()); | |
log.info("supportId: " + akamaiPurgeResp.getSupportId()); | |
log.info("--------------------------"); | |
if ( !awaitAkamai ) { | |
log.info("Akamai Flusher currently set to NOT await the purge completion!"); | |
return; | |
} | |
Integer waitTimeInSeconds = akamaiPurgeResp.getPingAfterSeconds(); | |
Integer totalWaitingTime = 0; | |
String progressUri = akamaiPurgeResp.getProgressUri(); | |
boolean purgeComplete = false; | |
do { | |
Thread.sleep( waitTimeInSeconds * 1000 ); | |
HttpGet getRequest = new HttpGet("https://api.ccu.akamai.com" + progressUri); | |
getRequest.setHeader(new BasicHeader("Content-Type", "application/json")); | |
getRequest.setHeader(new BasicHeader("Accept", "application/json")); | |
response = httpClient.execute(getRequest); | |
AkamaiStatusResponse akamaiStatusResp = mapper.readValue(response.getEntity().getContent(), AkamaiStatusResponse.class); | |
log.info("--------------------------"); | |
log.info("originalEstimatedSeconds: " + akamaiStatusResp.getOriginalEstimatedSeconds()); | |
log.info("progressUri: " + akamaiStatusResp.getProgressUri()); | |
log.info("originalQueueLength: " + akamaiStatusResp.getOriginalQueueLength()); | |
log.info("purgeId: " + akamaiStatusResp.getPurgeId()); | |
log.info("supportId: " + akamaiStatusResp.getSupportId()); | |
log.info("httpStatus: " + akamaiStatusResp.getHttpStatus()); | |
log.info("completionTime: " + akamaiStatusResp.getCompletionTime()); | |
log.info("submittedBy: " + akamaiStatusResp.getSubmittedBy()); | |
log.info("purgeStatus: " + akamaiStatusResp.getPurgeStatus()); | |
log.info("submissionTime:" + akamaiStatusResp.getSubmissionTime()); | |
log.info("pingAfterSeconds: " + akamaiStatusResp.getPingAfterSeconds()); | |
log.info("--------------------------"); | |
if ( akamaiStatusResp.getCompletionTime() == null ) { | |
totalWaitingTime += waitTimeInSeconds; | |
if ( totalWaitingTime >= 1800 ) { | |
throw new ProcessingException("Exceeded maximum waiting time (30 minutes) for purging Akamai"); | |
} | |
waitTimeInSeconds = akamaiStatusResp.getPingAfterSeconds(); | |
progressUri = akamaiStatusResp.getProgressUri(); | |
} else { | |
purgeComplete = true; | |
} | |
} while ( !purgeComplete ); | |
log.info("************************"); | |
log.info("PURGE REQUEST COMPLETED!"); | |
log.info("************************"); | |
} catch (UnsupportedEncodingException encodingException) { | |
log.error("Error inside the 'purgeUrls' method [" + encodingException.getMessage() + "]"); | |
throw new ProcessingException("Error purging Akamai [" + encodingException.getMessage() + "]"); | |
} catch (IOException ioException) { | |
log.error("Error inside the 'purgeUrls' method [" + ioException.getMessage() + "]"); | |
throw new ProcessingException("Error purging Akamai [" + ioException.getMessage() + "]"); | |
} catch (InterruptedException interruptedException) { | |
log.error("Error inside the 'purgeUrls' method [" + interruptedException.getMessage() + "]"); | |
throw new ProcessingException("Error purging Akamai [" + interruptedException.getMessage() + "]"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tridion.ps.akamai; | |
import org.codehaus.jackson.annotate.JsonIgnoreProperties; | |
@JsonIgnoreProperties(ignoreUnknown = true) | |
public class AkamaiStatusResponse extends AkamaiPurgeResponse { | |
private Integer originalEstimatedSeconds; | |
private Integer originalQueueLength; | |
private String completionTime; | |
private String submittedBy; | |
private String purgeStatus; | |
private String submissionTime; | |
public Integer getOriginalEstimatedSeconds() { | |
return originalEstimatedSeconds; | |
} | |
public void setOriginalEstimatedSeconds(Integer originalEstimatedSeconds) { | |
this.originalEstimatedSeconds = originalEstimatedSeconds; | |
} | |
public Integer getOriginalQueueLength() { | |
return originalQueueLength; | |
} | |
public void setOriginalQueueLength(Integer originalQueueLength) { | |
this.originalQueueLength = originalQueueLength; | |
} | |
public String getCompletionTime() { | |
return completionTime; | |
} | |
public void setCompletionTime(String completionTime) { | |
this.completionTime = completionTime; | |
} | |
public String getSubmittedBy() { | |
return submittedBy; | |
} | |
public void setSubmittedBy(String submittedBy) { | |
this.submittedBy = submittedBy; | |
} | |
public String getPurgeStatus() { | |
return purgeStatus; | |
} | |
public void setPurgeStatus(String purgeStatus) { | |
this.purgeStatus = purgeStatus; | |
} | |
public String getSubmissionTime() { | |
return submissionTime; | |
} | |
public void setSubmissionTime(String submissionTime) { | |
this.submissionTime = submissionTime; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Module Type="CustomAkamaiFlusher" Class="com.tridion.ps.akamai.AkamaiRestFlusher"> | |
<AkamaiUsername>akamaiusername@userdomain.com</AkamaiUsername> | |
<AkamaiPassword>*************</AkamaiPassword> | |
<AkamaiDomain>Staging</AkamaiDomain> | |
<AwaitPurgeCompletion>False</AwaitPurgeCompletion> | |
<WebsiteDomains>/akamai-flusher/publications-domain.properties</WebsiteDomains> | |
</Module> |