/*
* #%L
* ACS AEM Tools Bundle
* %%
* Copyright (C) 2013 Adobe
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.adobe.acs.tools.test_page_generator.impl;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.wcm.api.NameConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import com.day.cq.wcm.api.Template;
import com.day.cq.wcm.api.WCMException;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.sling.SlingServlet;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@SlingServlet(
label = "ACS AEM Tools - Test Page Generator",
description = "Test page generator utility servlet end-point",
methods = { "POST" },
resourceTypes = { "acs-tools/components/test-page-generator" },
selectors = { "generate-pages" },
extensions = { "json" }
)
public class TestPageGeneratorServlet extends SlingAllMethodsServlet {
private static final Logger log = LoggerFactory.getLogger(TestPageGeneratorServlet.class);
@Reference
private ScriptEngineManager scriptEngineManager;
private static final int MILLIS_IN_SECONDS = 1000;
private static final String NT_SLING_FOLDER = "sling:Folder";
private static final String NODE_PREFIX = "test-page-";
private static final String TITLE_PREFIX = "Test Page ";
@Override
protected final void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
try {
request.getResourceResolver().adaptTo(Session.class).getWorkspace().getObservationManager().setUserData("acs-aem-tools.test-page-generator");
final JSONObject json = this.generatePages(request.getResourceResolver(), new Parameters(request));
response.getWriter().write(json.toString(2));
} catch (JSONException e) {
log.error(e.getMessage());
this.sendJSONError(response,
"Form errors",
"Could not understand provided parameters");
} catch (RepositoryException e) {
log.error("Could not perform interim Save due to: {}", e.getMessage());
this.sendJSONError(response,
"Repository error",
e.getMessage());
} catch (WCMException e) {
log.error("Could not create Page due to: {}", e.getMessage());
this.sendJSONError(response,
"WCM Page creation error",
e.getMessage());
} catch (IllegalArgumentException e) {
log.error("Could not store JavaScript eval result into repository: {}", e.getMessage());
this.sendJSONError(response,
"JavaScript-based property evaluation error",
e.getMessage());
}
}
private JSONObject generatePages(ResourceResolver resourceResolver, Parameters parameters) throws IOException,
WCMException, RepositoryException, JSONException {
final ScriptEngine scriptEngine = scriptEngineManager.getEngineByExtension("ecma");
final JSONObject jsonResponse = new JSONObject();
final int pageCount = parameters.getTotal();
final int bucketSize = parameters.getBucketSize();
final int saveThreshold = parameters.getSaveThreshold();
final String rootPath = parameters.getRootPath();
final Session session = resourceResolver.adaptTo(Session.class);
/* Initialize Depth Tracker */
int[] depthTracker = this.initDepthTracker(pageCount, bucketSize);
int i = 0;
int bucketCount = 0;
long start = System.currentTimeMillis();
while (i++ < pageCount) {
depthTracker = this.updateDepthTracker(depthTracker, bucketCount, bucketSize);
if (this.needsNewBucket(bucketCount, bucketSize)) {
bucketCount = 0;
}
final String folderPath = this.getOrCreateBucketPath(resourceResolver, parameters, rootPath, depthTracker);
final Page page = createPage(resourceResolver,
folderPath,
NODE_PREFIX + (i + 1),
parameters.getTemplate(),
TITLE_PREFIX + (i + 1));
final ModifiableValueMap properties = page.getContentResource().adaptTo(ModifiableValueMap.class);
for (Map.Entry<String, Object> entry : parameters.getProperties().entrySet()) {
properties.put(entry.getKey(), this.eval(scriptEngine, entry.getValue()));
}
bucketCount++;
if (i % saveThreshold == 0) {
log.debug("Saving at threshold for [ {} ] items", i);
this.save(session);
}
}
if (saveThreshold % i != 0) {
this.save(session);
}
jsonResponse.put("totalTime", (int) ((System.currentTimeMillis() - start) / MILLIS_IN_SECONDS));
jsonResponse.put("rootPath", rootPath);
jsonResponse.put("bucketSize", bucketSize);
jsonResponse.put("saveThreshold", saveThreshold);
jsonResponse.put("depth", depthTracker.length);
jsonResponse.put("count", pageCount);
jsonResponse.put("success", true);
return jsonResponse;
}
private void sendJSONError(SlingHttpServletResponse response, String title, String message) throws IOException {
final JSONObject json = new JSONObject();
response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
json.put("title", title);
json.put("message", message);
response.getWriter().write(json.toString());
} catch (JSONException e) {
String fallbackJSON = "{ \"title\": \"Error creating error response. "
+ "Please review AEM error logs.\" }";
response.getWriter().write(fallbackJSON);
}
}
/**
* Saves the current state.
*
* @param session session obj
* @return the time it took to save
* @throws RepositoryException
*/
private long save(Session session) throws RepositoryException {
final long start = System.currentTimeMillis();
session.save();
final long total = System.currentTimeMillis() - start;
log.debug("Save operation for batch page creation took {} ms", total);
return total;
}
/**
* Determines if a new bucket is need (if the current bucket is full).
*
* @param bucketCount number of items in the bucket
* @param bucketSize the max number of items to be added to a bucket
* @return true if a new bucket is required
*/
private boolean needsNewBucket(int bucketCount, int bucketSize) {
return bucketCount >= bucketSize;
}
/**
* Creates the parent bucket structure to place the Page.
*
* @param resourceResolver the resource resolver
* @param rootPath the root path used for the test page generation
* @param depthTracker the depth tracker indicating the current bucket
* @return the path to the newly created bucket
* @throws RepositoryException
*/
private String getOrCreateBucketPath(ResourceResolver resourceResolver, Parameters params, String rootPath, int[]
depthTracker)
throws RepositoryException {
final Session session = resourceResolver.adaptTo(Session.class);
String folderPath = rootPath;
for (int i = 0; i < depthTracker.length; i++) {
final String tmp = Integer.toString(depthTracker[i] + 1);
folderPath += "/" + tmp;
}
if (resourceResolver.getResource(folderPath) != null) {
return folderPath;
} else {
Node node;
if (StringUtils.equals(NT_SLING_FOLDER, params.getBucketType())) {
// Create bucket structure as sling:Folders
node = JcrUtil.createPath(folderPath, NT_SLING_FOLDER, NT_SLING_FOLDER, session, false);
JcrUtil.createPath(folderPath + "/jcr:content", NameConstants.NT_PAGE, JcrConstants.NT_UNSTRUCTURED,
session, false);
log.debug("Created new folder path at [ {} ]", node.getPath());
} else {
// Create bucket structure as cq:Pages
node = JcrUtil.createPath(folderPath, NameConstants.NT_PAGE, NameConstants.NT_PAGE, session, false);
JcrUtil.createPath(folderPath + "/jcr:content", NameConstants.NT_PAGE, "cq:PageContent",
session, false);
}
return node.getPath();
}
}
/**
* Creates and initializes the depth tracker array.
*
* @param total total number of pages to create
* @param bucketSize size of each bucket
* @return the depth tracker array initialized to all 0's
*/
private int[] initDepthTracker(int total, int bucketSize) {
int depth = getDepth(total, bucketSize);
int[] depthTracker = new int[depth];
for (int i = 0; i < depthTracker.length; i++) {
depthTracker[i] = 0;
}
return depthTracker;
}
/**
* Manages tracker used to determine the parent bucket structure.
*
* @param depthTracker Array used to track which bucket is used to create the "current" page
* @param bucketCount Number of items already in the bucket
* @param bucketSize The max number of items in the bucket
* @return The updated depth tracker array
*/
private int[] updateDepthTracker(int[] depthTracker, int bucketCount, int bucketSize) {
if (!this.needsNewBucket(bucketCount, bucketSize)) {
return depthTracker;
}
for (int i = depthTracker.length - 1; i >= 0; i--) {
if (depthTracker[i] >= bucketSize - 1) {
depthTracker[i] = 0;
} else {
depthTracker[i] = depthTracker[i] + 1;
log.debug("Updating depthTracker at location [ {} ] to [ {} ]", i, depthTracker[i]);
break;
}
}
return depthTracker;
}
/**
* Determines the bucket depth required to organize the pages so no more than bucketSize siblings ever exist.
*
* @param total Total number pages to create
* @param bucketSize Max number of siblings
* @return The node depth required to achieve desired bucket-size
*/
private int getDepth(int total, int bucketSize) {
int depth = 0;
int remainingSize = total;
do {
remainingSize = (int) Math.ceil((double) remainingSize / (double) bucketSize);
log.debug("Remaining size of [ {} ] at depth [ {} ]", remainingSize, depth);
depth++;
} while (remainingSize > bucketSize);
log.debug("Final depth of [ {} ]", depth);
return depth;
}
/**
* Wrapper for CQ PageManager API since it does not create the jcr:content node with jcr:primaryType=cq:PageContent.
*
* @param resourceResolver the resource resolver
* @param folderPath the path to create page (must exist)
* @param nodeName the name of the node; if node of this name already exists a unique name will be generated
* @param templatePath the absolute path to the template to use
* @param title the jcr:title of the page
* @return the new Page
* @throws WCMException could not create the page using CQ PageManager API
* @throws RepositoryException could not find folderPath node
*/
private Page createPage(ResourceResolver resourceResolver, String folderPath, String nodeName, String templatePath,
String title) throws RepositoryException, WCMException {
final PageManager pageManager = resourceResolver.adaptTo(PageManager.class);
final Template template = pageManager.getTemplate(templatePath);
if (template != null) {
// A template is defined so use that
return pageManager.create(folderPath, nodeName, templatePath, title, false);
} else {
// Manually create the page nodes to prevent the creation of nt:unstructured-based jcr:content node
final Session session = resourceResolver.adaptTo(Session.class);
final Node folderNode = session.getNode(folderPath);
nodeName = JcrUtil.createValidName(nodeName);
final Node pageNode = JcrUtil.createUniqueNode(folderNode, nodeName, NameConstants.NT_PAGE, session);
final Node contentNode = JcrUtil.createUniqueNode(pageNode, JcrConstants.JCR_CONTENT,
"cq:PageContent", session);
JcrUtil.setProperty(contentNode, JcrConstants.JCR_TITLE, title);
return resourceResolver.getResource(pageNode.getPath()).adaptTo(Page.class);
}
}
private Object eval(final ScriptEngine scriptEngine, final Object value) {
if (scriptEngine == null) {
log.warn("ScriptEngine is null; cannot evaluate");
return value;
} else if (value instanceof String[]) {
final List<String> scripts = new ArrayList<String>();
final String[] values = (String[]) value;
for (final String val : values) {
scripts.add(String.valueOf(this.eval(scriptEngine, val)));
}
return scripts.toArray(new String[scripts.size()]);
} else if (!(value instanceof String)) {
return value;
}
final String stringValue = StringUtils.stripToEmpty((String) value);
String script;
if (StringUtils.startsWith(stringValue, "{{")
&& StringUtils.endsWith(stringValue, "}}")) {
script = StringUtils.removeStart(stringValue, "{{");
script = StringUtils.removeEnd(script, "}}");
script = StringUtils.stripToEmpty(script);
try {
return scriptEngine.eval(script);
} catch (ScriptException e) {
log.error("Could not evaluation the test page property ecma [ {} ]", script);
}
}
return value;
}
}