From a342f4fc56e7d56e79a13125b78fdceba49f75f5 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov <yegor.kozlov@gmail.com> Date: Sat, 4 Jan 2025 17:46:21 +0100 Subject: [PATCH] import from excel to shard redirects > 1K --- CHANGELOG.md | 13 + all/pom.xml | 2 +- bundle-cloud/pom.xml | 2 +- bundle-onprem/pom.xml | 2 +- bundle/pom.xml | 2 +- .../redirects/filter/RedirectFilter.java | 19 +- .../redirects/models/Configurations.java | 6 - .../redirects/models/ExportColumn.java | 2 +- .../commons/redirects/models/ImportLog.java | 2 +- .../models/RedirectConfiguration.java | 20 +- .../redirects/models/RedirectRule.java | 6 +- .../commons/redirects/models/Redirects.java | 32 +- .../redirects/models/package-info.java | 2 +- .../CreateRedirectConfigurationServlet.java | 1 - .../servlets/ExportRedirectMapServlet.java | 19 +- .../servlets/ImportRedirectMapServlet.java | 87 ++-- .../redirects/servlets/RewriteMapServlet.java | 30 +- .../redirects/RedirectResourceBuilder.java | 3 +- .../ExportRedirectMapServletTest.java | 20 +- .../ImportRedirectMapServletTest.java | 383 +++++++++--------- .../servlets/RewriteMapServletTest.java | 23 +- oakpal-checks/pom.xml | 2 +- pom.xml | 2 +- ui.apps/pom.xml | 2 +- .../manage-redirects/clientlibs/app.js | 5 + .../manage-redirects/manage-redirects.html | 15 +- .../content/redirect-manager/.content.xml | 6 + ui.config/pom.xml | 2 +- ui.content/pom.xml | 2 +- 29 files changed, 416 insertions(+), 296 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e276ea6a..e4d0e22bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) <!-- Keep this up to date! After a release, change the tag name to the latest release -->- ## Unreleased ([details][unreleased changes details]) + +- ### Added +#3501 Redirect Manager: Large-Scale Import Optimization + +### Fixed +- #3497 - Redirect Manager: allow creating redirect configurations in a nested hierarchy + +## 6.10.0 - 2024-12-13 + +### Fixed +- #3497 - Redirect Manager: allow creating redirect configurations in a nested hierarchy + +## 6.10.0 - 2024-12-13 ### Changed diff --git a/all/pom.xml b/all/pom.xml index 0df358683f..0e23987021 100644 --- a/all/pom.xml +++ b/all/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/bundle-cloud/pom.xml b/bundle-cloud/pom.xml index 1ed95497fe..db5070a175 100644 --- a/bundle-cloud/pom.xml +++ b/bundle-cloud/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/bundle-onprem/pom.xml b/bundle-onprem/pom.xml index c7d4ef2740..87c79227a4 100644 --- a/bundle-onprem/pom.xml +++ b/bundle-onprem/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/bundle/pom.xml b/bundle/pom.xml index 693a70a4a0..ed60219be4 100644 --- a/bundle/pom.xml +++ b/bundle/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/filter/RedirectFilter.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/filter/RedirectFilter.java index f3e94f4acb..1ebb229eca 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/filter/RedirectFilter.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/filter/RedirectFilter.java @@ -71,6 +71,7 @@ import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestPathInfo; +import org.apache.sling.api.resource.AbstractResourceVisitor; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceUtil; @@ -101,6 +102,7 @@ import org.slf4j.LoggerFactory; import static com.adobe.acs.commons.redirects.models.Redirects.CFG_PROP_IGNORE_SELECTORS; +import static com.adobe.acs.commons.redirects.models.Redirects.readRedirects; import static org.apache.sling.engine.EngineConstants.SLING_FILTER_SCOPE; import static org.osgi.framework.Constants.SERVICE_DESCRIPTION; import static org.osgi.framework.Constants.SERVICE_ID; @@ -332,15 +334,14 @@ RedirectConfiguration loadRules(Resource storageResource) { } public static Collection<RedirectRule> getRules(Resource resource) { - Collection<RedirectRule> rules = new ArrayList<>(); - for (Resource res : resource.getChildren()) { - if(res.isResourceType(REDIRECT_RULE_RESOURCE_TYPE)){ - RedirectRule rule = res.adaptTo(RedirectRule.class); - if(rule != null) { - rules.add(rule); - } - } - } + List<Resource> resources = readRedirects(resource); + long t0 = System.currentTimeMillis(); + Collection<RedirectRule> rules = resources + .stream() + .map(res -> res.adaptTo(RedirectRule.class)) + .filter(res -> res != null) + .collect(Collectors.toList()); + log.trace("mapped {} models in {} ms", resources.size(), System.currentTimeMillis() - t0); return rules; } diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Configurations.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Configurations.java index 4a20dceece..d3349f324a 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Configurations.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Configurations.java @@ -25,16 +25,10 @@ import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.SlingObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.jcr.query.Query; -import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; import java.util.List; /** diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ExportColumn.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ExportColumn.java index 56a6fcb38f..55091e2eb3 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ExportColumn.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ExportColumn.java @@ -36,7 +36,7 @@ public enum ExportColumn { CREATED_BY("Created By", RedirectRule.CREATED_BY_PROPERTY_NAME, String.class, false), MODIFIED("Modified", RedirectRule.MODIFIED_PROPERTY_NAME, Calendar.class, false), MODIFIED_BY("Modified By", RedirectRule.MODIFIED_BY_PROPERTY_NAME, String.class, false), - CACHE_CONTROL("Cache-Control", RedirectRule.CACHE_CONTROL_HEADER_NAME, String.class, false); + CACHE_CONTROL("Cache-Control", RedirectRule.CACHE_CONTROL_HEADER_NAME, String.class, true); private final String title; private final String propertyName; diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ImportLog.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ImportLog.java index 9694c28072..c0ae15c99a 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ImportLog.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/ImportLog.java @@ -80,7 +80,7 @@ public String getMsg() { } } - enum Level { + public enum Level { WARN, INFO } diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectConfiguration.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectConfiguration.java index b2c52e4f06..c1d3999984 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectConfiguration.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectConfiguration.java @@ -56,16 +56,25 @@ public class RedirectConfiguration { private RedirectConfiguration(){ pathRules = new LinkedHashMap<>(); + caseInsensitiveRules = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); patternRules = new LinkedHashMap<>(); } public RedirectConfiguration(Resource resource, String storageSuffix) { - pathRules = new LinkedHashMap<>(); - caseInsensitiveRules = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - patternRules = new LinkedHashMap<>(); - path = resource.getPath(); + this(resource, storageSuffix, true); + } + + RedirectConfiguration(Resource configResource, String storageSuffix, boolean loadRules) { + this(); + path = configResource.getPath(); name = path.replace("/" + storageSuffix, ""); - Collection<RedirectRule> rules = RedirectFilter.getRules(resource); + if(loadRules){ + loadRules(configResource); + } + } + + void loadRules(Resource configResource) { + Collection<RedirectRule> rules = RedirectFilter.getRules(configResource); for (RedirectRule rule : rules) { if (rule.getRegex() != null) { patternRules.put(rule.getRegex(), rule); @@ -84,7 +93,6 @@ public RedirectConfiguration(Resource resource, String storageSuffix) { /** * @return resource path without .html extension */ - public static String normalizePath(String resourcePath) { int sep = resourcePath.lastIndexOf('.'); if (sep != -1 && !resourcePath.startsWith("/content/dam/")) { diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectRule.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectRule.java index 3e4435688a..f7f9573a2a 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectRule.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/RedirectRule.java @@ -148,8 +148,6 @@ protected void init() { source = source.trim(); target = target.trim(); - createdBy = AuthorizableUtil.getFormattedName(resource.getResourceResolver(), createdBy); - modifiedBy = AuthorizableUtil.getFormattedName(resource.getResourceResolver(), modifiedBy); String regex = source; if (regex.endsWith("*")) { @@ -187,11 +185,11 @@ public boolean isCaseInsensitive() { } public String getCreatedBy() { - return createdBy; + return AuthorizableUtil.getFormattedName(resource.getResourceResolver(), createdBy); } public String getModifiedBy() { - return modifiedBy; + return AuthorizableUtil.getFormattedName(resource.getResourceResolver(), modifiedBy); } public boolean getContextPrefixIgnored() { diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Redirects.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Redirects.java index 74301c937b..20d9aa6a57 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Redirects.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/Redirects.java @@ -32,6 +32,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.AbstractResourceVisitor; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ValueMap; @@ -39,7 +40,10 @@ import org.apache.sling.models.annotations.injectorspecific.OSGiService; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import static com.adobe.acs.commons.redirects.filter.RedirectFilter.REDIRECT_RULE_RESOURCE_TYPE; import static com.adobe.acs.commons.redirects.models.RedirectRule.*; /** @@ -50,6 +54,8 @@ public class Redirects { public static final String CFG_PROP_CONTEXT_PREFIX = "contextPrefix"; public static final String CFG_PROP_IGNORE_SELECTORS = "ignoreSelectors"; + public static final String CFG_PROP_PAGE_SIZE = "pageSize"; + private static final Logger log = LoggerFactory.getLogger(Redirects.class); @SlingObject private SlingHttpServletRequest request; @@ -72,8 +78,7 @@ protected void init() { ValueMap properties = configResource.getValueMap(); contextPrefix = properties.get(Redirects.CFG_PROP_CONTEXT_PREFIX, ""); ignoreSelectors = properties.get(Redirects.CFG_PROP_IGNORE_SELECTORS, false); - - List<Resource> all = new ArrayList<>(); + pageSize = properties.get(Redirects.CFG_PROP_PAGE_SIZE, 100); if (ArrayUtils.contains(request.getRequestPathInfo().getSelectors(), "search")) { // Search @@ -85,11 +90,32 @@ protected void init() { if (pg != null) { pageNumber = Integer.parseInt(pg); } - configResource.listChildren().forEachRemaining(all::add); + List<Resource> all = readRedirects(configResource); pages = Lists.partition(all, pageSize); } } + /** + * Read redirects stored in AEM + * + * @param configResource the configuration resource, e.g. /conf/my-site/settings/redirects + * @return list of collected redirect resources + */ + public static List<Resource> readRedirects(Resource configResource) { + long t0 = System.currentTimeMillis(); + List<Resource> redirects = new ArrayList<>(); + new AbstractResourceVisitor() { + @Override + public void visit(Resource res) { + if(res.isResourceType(REDIRECT_RULE_RESOURCE_TYPE)){ + redirects.add(res); + } + } + }.accept(configResource); + log.debug("Read {} redirects from {} in {}ms", redirects.size(), configResource.getPath(), System.currentTimeMillis() - t0); + return redirects; + } + public List<Resource> getItems() { return pages.isEmpty() ? Collections.emptyList() : pages.get(pageNumber - 1); } diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/package-info.java index f800337c61..08bb6a4ed9 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/models/package-info.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/models/package-info.java @@ -15,5 +15,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@org.osgi.annotation.versioning.Version("6.1.0") +@org.osgi.annotation.versioning.Version("6.11.0") package com.adobe.acs.commons.redirects.models; diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/CreateRedirectConfigurationServlet.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/CreateRedirectConfigurationServlet.java index b92cf63280..fca967bf0d 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/CreateRedirectConfigurationServlet.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/CreateRedirectConfigurationServlet.java @@ -28,7 +28,6 @@ import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.api.resource.ResourceUtil; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.jcr.resource.api.JcrResourceConstants; import org.osgi.service.component.annotations.Component; diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServlet.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServlet.java index f2558e8fad..58b17b6a1e 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServlet.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServlet.java @@ -27,9 +27,9 @@ import org.apache.poi.ss.usermodel.Font; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.Resource; @@ -61,7 +61,7 @@ public class ExportRedirectMapServlet extends SlingSafeMethodsServlet { private static final Logger log = LoggerFactory.getLogger(ExportRedirectMapServlet.class); private static final long serialVersionUID = -3564475196678277711L; - static final String SPREADSHEETML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + static final String CONTENT_TYPE_EXCEL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) @@ -72,16 +72,17 @@ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse r log.debug("Requesting redirect maps from {}", path); Collection<RedirectRule> rules = RedirectFilter.getRules(root); - XSSFWorkbook wb = export(rules); + Workbook wb = export(rules); - response.setContentType(SPREADSHEETML_SHEET); - response.setHeader("Content-Disposition", "attachment;filename=\"acs-redirects.xlsx\" "); + response.setContentType(CONTENT_TYPE_EXCEL); + String fileName = root.getParent().getParent().getName() + "-redirects"; + response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + ".xlsx\" "); wb.write(response.getOutputStream()); } - static XSSFWorkbook export(Collection<RedirectRule> rules) { - XSSFWorkbook wb = new XSSFWorkbook(); - XSSFCellStyle headerStyle = wb.createCellStyle(); + static Workbook export(Collection<RedirectRule> rules) { + Workbook wb = new SXSSFWorkbook(); + CellStyle headerStyle = wb.createCellStyle(); headerStyle.setFillForegroundColor(IndexedColors.DARK_BLUE.getIndex()); headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); Font headerFont = wb.createFont(); diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServlet.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServlet.java index 1044231c4b..2c367c59a3 100755 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServlet.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServlet.java @@ -54,9 +54,10 @@ import java.util.Collection; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.HashMap; -import java.util.LinkedHashSet; import java.util.LinkedHashMap; import java.util.Calendar; import java.util.HashSet; @@ -66,7 +67,10 @@ import static com.adobe.acs.commons.redirects.filter.RedirectFilter.ACS_REDIRECTS_RESOURCE_TYPE; import static com.adobe.acs.commons.redirects.filter.RedirectFilter.REDIRECT_RULE_RESOURCE_TYPE; import static com.adobe.acs.commons.redirects.models.RedirectRule.SOURCE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.Redirects.readRedirects; import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES; +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; +import static org.apache.jackrabbit.JcrConstants.NT_UNSTRUCTURED; import static org.apache.sling.api.resource.ResourceResolver.PROPERTY_RESOURCE_TYPE; /** @@ -86,6 +90,7 @@ public class ImportRedirectMapServlet extends SlingAllMethodsServlet { private static final String MIX_CREATED = "mix:created"; private static final String MIX_LAST_MODIFIED = "mix:lastModified"; private static final String AUDIT_LOG_FOLDER = "/var/acs-commons/redirects"; + private static final int SHARD_SIZE = 1000; @Override protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) @@ -94,13 +99,23 @@ protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response.setContentType("application/json"); String path = request.getParameter("path"); + boolean replace = request.getParameter("replace") != null; Resource storageRoot = request.getResourceResolver().getResource(path); log.debug("Updating redirect maps at {}", storageRoot.getPath()); - Map<String, Resource> jcrRules = getRules(storageRoot); // rules stored in crx + Map<String, Resource> jcrRules; + if(replace){ + jcrRules = Collections.emptyMap(); + for(Resource ch : storageRoot.getChildren()){ + ch.getResourceResolver().delete(ch); + } + } else { + jcrRules = getRules(storageRoot); // rules stored in crx + } + ImportLog auditLog = new ImportLog(); Collection<Map<String, Object>> xlsRules; try (InputStream is = getFile(request)) { - xlsRules = readEntries(is, auditLog); // rules read from excel + xlsRules = readEntries(is, auditLog); // rules read from Excel } if (!xlsRules.isEmpty()) { update(storageRoot, xlsRules, jcrRules); @@ -114,30 +129,51 @@ protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse * @return redirect nodes keyed by source path */ Map<String, Resource> getRules(Resource resource) { - Map<String, Resource> rules = new LinkedHashMap<>(); - for (Resource res : resource.getChildren()) { - if (res.isResourceType(REDIRECT_RULE_RESOURCE_TYPE)) { - String src = res.getValueMap().get(SOURCE_PROPERTY_NAME, String.class); - rules.put(src, res); - } + List<Resource> redirects = readRedirects(resource); + Map<String, Resource> rulesByPathMap = new LinkedHashMap<>(); + for(Resource res : redirects){ + String src = res.getValueMap().get(SOURCE_PROPERTY_NAME, String.class); + rulesByPathMap.put(src, res); } - return rules; + return rulesByPathMap; } /** - * @param root root resource, e.g. /conf/global/settings/redirects - * @param xlsRules redirects read from an Excel spreadsheet - * @param jcrRedirects existing redirect nodes keyed by the source path. - * We assume that the source path is unique. + * @param root root resource, e.g. /conf/global/settings/redirects + * @param xlsRedirects redirects read from an Excel spreadsheet + * @param jcrRedirects existing redirect nodes keyed by the source path. + * We assume that the source path is unique. */ - void update(Resource root, Collection<Map<String, Object>> xlsRules, Map<String, Resource> jcrRedirects) throws PersistenceException { + void update(Resource root, Collection<Map<String, Object>> xlsRedirects, Map<String, Resource> jcrRedirects) throws PersistenceException { ResourceResolver resolver = root.getResourceResolver(); - for (Map<String, Object> props : xlsRules) { - String sourcePath = (String) props.get(SOURCE_PROPERTY_NAME); - Resource redirect = getOrCreateRedirect(root, sourcePath, props, jcrRedirects); - log.debug("rule: {}", redirect.getPath()); + long t0 = System.currentTimeMillis(); + + if(xlsRedirects.size() > SHARD_SIZE){ + int count = 0; + for (Map<String, Object> props : xlsRedirects) { + count++; + + String shardName = "shard-" + count / SHARD_SIZE; + String sourcePath = (String) props.get(SOURCE_PROPERTY_NAME); + Resource shard = root.getChild(shardName); + if(shard == null){ + shard = resolver.create(root, shardName, Collections.singletonMap(JCR_PRIMARYTYPE, NT_UNSTRUCTURED)); + } + Resource redirect = getOrCreateRedirect(shard, sourcePath, props, jcrRedirects); + log.trace("rule[{}]: {}", count, redirect.getPath()); + if(count % SHARD_SIZE == 0) { + resolver.commit(); + } + } + } else { + for (Map<String, Object> props : xlsRedirects) { + String sourcePath = (String) props.get(SOURCE_PROPERTY_NAME); + Resource redirect = getOrCreateRedirect(root, sourcePath, props, jcrRedirects); + log.trace("rule: {}", redirect.getPath()); + } + resolver.commit(); } - resolver.commit(); + log.debug("{} rules imported in {}ms", xlsRedirects.size(), System.currentTimeMillis() - t0); } private Resource getOrCreateRedirect(Resource root, String sourcePath, Map<String, Object> props, Map<String, Resource> jcrRedirects) throws PersistenceException { @@ -145,6 +181,7 @@ private Resource getOrCreateRedirect(Resource root, String sourcePath, Map<Strin if (redirect == null) { // add mix:created, AEM will initialize jcr:created and jcr:createdBy from the current session props.put(JCR_MIXINTYPES, MIX_CREATED); + props.put(JCR_PRIMARYTYPE, NT_UNSTRUCTURED); String nodeName = ResourceUtil.createUniqueChildName(root, "redirect-rule-"); redirect = root.getResourceResolver().create(root, nodeName, props); } else { @@ -225,21 +262,21 @@ private Map<String, Object> readRedirect(Row row, Map<ExportColumn, Integer> col Map<String, Object> props = new HashMap<>(); props.put(PROPERTY_RESOURCE_TYPE, REDIRECT_RULE_RESOURCE_TYPE); Cell c0 = row.getCell(0); - if (c0 == null || c0.getCellType() != CellType.STRING) { + if (c0 == null || c0.getCellType() != CellType.STRING || c0.toString().isEmpty()) { auditLog.warn(new CellReference(row.getRowNum(), 0).formatAsString(), - "Cells A is required and should contain redirect source"); + "Cell A is required and should contain redirect source"); return null; } Cell c1 = row.getCell(1); - if (c1 == null || c1.getCellType() != CellType.STRING) { + if (c1 == null || c1.getCellType() != CellType.STRING || c1.toString().isEmpty()) { auditLog.warn(new CellReference(row.getRowNum(), 1).formatAsString(), - "Cells B is required and should contain redirect source"); + "Cell B is required and should contain redirect source"); return null; } Cell c2 = row.getCell(2); if (c2 == null || c2.getCellType() != CellType.NUMERIC) { auditLog.warn(new CellReference(row.getRowNum(), 2).formatAsString(), - "Cells C is required and should contain redirect status code"); + "Cell C is required and should contain redirect status code"); return null; } String source = c0.getStringCellValue(); diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServlet.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServlet.java index b1a1ca345e..9a3da18f73 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServlet.java +++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServlet.java @@ -19,11 +19,11 @@ */ package com.adobe.acs.commons.redirects.servlets; -import com.adobe.acs.commons.redirects.filter.RedirectFilter; -import com.adobe.acs.commons.redirects.models.RedirectRule; import org.apache.http.entity.ContentType; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.osgi.service.component.annotations.Component; @@ -33,6 +33,11 @@ import java.io.PrintWriter; import java.util.Collection; +import static com.adobe.acs.commons.redirects.models.RedirectRule.NOTE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.SOURCE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.STATUS_CODE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.TARGET_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.Redirects.readRedirects; import static com.adobe.acs.commons.redirects.servlets.CreateRedirectConfigurationServlet.REDIRECTS_RESOURCE_PATH; @@ -62,22 +67,27 @@ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse r response.setContentType(ContentType.TEXT_PLAIN.getMimeType()); String[] selectors = request.getRequestPathInfo().getSelectors(); - int statusCode = 0; + int statusCodeSelector = 0; if(selectors != null && selectors.length > 0) { - statusCode = Integer.parseInt(selectors[0]); + statusCodeSelector = Integer.parseInt(selectors[0]); } - Collection<RedirectRule> rules = RedirectFilter.getRules(request.getResource()); + Collection<Resource> rules = readRedirects(request.getResource()); PrintWriter out = response.getWriter(); - out.printf("# %s Redirects\n", statusCode == 0 ? "All" : "" + statusCode); - for (RedirectRule rule : rules) { - if(statusCode != 0 && rule.getStatusCode() != statusCode) { + out.printf("# %s Redirects\n", statusCodeSelector == 0 ? "All" : "" + statusCodeSelector); + for (Resource resource : rules) { + ValueMap props = resource.getValueMap(); + String source = props.get(SOURCE_PROPERTY_NAME, String.class); + String target = props.get(TARGET_PROPERTY_NAME, String.class); + int statusCode = props.get(STATUS_CODE_PROPERTY_NAME, Integer.class); + String note = props.get(NOTE_PROPERTY_NAME, String.class); + + if(statusCodeSelector != 0 && statusCodeSelector != statusCode) { continue; } - String note = rule.getNote(); if(note != null && !note.isEmpty()) { out.printf("# %s\n", note); } - out.printf("%s %s\n", rule.getSource(), rule.getTarget()); + out.printf("%s %s\n", source.trim(), target.trim()); } } } diff --git a/bundle/src/test/java/com/adobe/acs/commons/redirects/RedirectResourceBuilder.java b/bundle/src/test/java/com/adobe/acs/commons/redirects/RedirectResourceBuilder.java index 77a9212426..b9f1c9d9a6 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/redirects/RedirectResourceBuilder.java +++ b/bundle/src/test/java/com/adobe/acs/commons/redirects/RedirectResourceBuilder.java @@ -29,6 +29,7 @@ import static com.adobe.acs.commons.redirects.filter.RedirectFilter.REDIRECT_RULE_RESOURCE_TYPE; import static com.adobe.acs.commons.redirects.models.RedirectRule.*; +import static com.adobe.acs.commons.redirects.servlets.CreateRedirectConfigurationServlet.REDIRECTS_RESOURCE_PATH; public class RedirectResourceBuilder { public static final String DEFAULT_CONF_PATH = "/conf/global/settings/redirects"; @@ -137,7 +138,7 @@ public RedirectResourceBuilder setCaseInsensitive(boolean nc) { public Resource build() throws PersistenceException { ContentBuilder cb = context.create(); Resource configResource = ResourceUtil.getOrCreateResource( - context.resourceResolver(), configPath, REDIRECT_RULE_RESOURCE_TYPE, null, true); + context.resourceResolver(), configPath, REDIRECTS_RESOURCE_PATH, null, true); if(nodeName == null) { nodeName = ResourceUtil.createUniqueChildName(configResource, "rule"); } diff --git a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServletTest.java b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServletTest.java index b70a7f3ad3..d30b8a9674 100755 --- a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServletTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ExportRedirectMapServletTest.java @@ -20,8 +20,10 @@ import com.adobe.acs.commons.redirects.filter.RedirectFilter; import com.adobe.acs.commons.redirects.RedirectResourceBuilder; import com.adobe.acs.commons.redirects.models.RedirectRule; -import org.apache.poi.xssf.usermodel.XSSFRow; -import org.apache.poi.xssf.usermodel.XSSFSheet; + +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; @@ -40,7 +42,7 @@ import java.util.Collection; import static com.adobe.acs.commons.redirects.Asserts.assertDateEquals; -import static com.adobe.acs.commons.redirects.servlets.ExportRedirectMapServlet.SPREADSHEETML_SHEET; +import static com.adobe.acs.commons.redirects.servlets.ExportRedirectMapServlet.CONTENT_TYPE_EXCEL; import static org.junit.Assert.*; /** @@ -90,7 +92,7 @@ public void testGet() throws ServletException, IOException { servlet.doGet(request, response); - assertEquals(SPREADSHEETML_SHEET, response.getContentType()); + assertEquals(CONTENT_TYPE_EXCEL, response.getContentType()); // read the generated spreadsheet XSSFWorkbook wb = new XSSFWorkbook(new ByteArrayInputStream(response.getOutput())); assertSpreadsheet(wb); @@ -101,14 +103,14 @@ public void testExport() { Resource resource = context.resourceResolver().getResource(redirectStoragePath); Collection<RedirectRule> rules = RedirectFilter.getRules(resource); - XSSFWorkbook wb = servlet.export(rules); + Workbook wb = ExportRedirectMapServlet.export(rules); assertSpreadsheet(wb); } - public void assertSpreadsheet(XSSFWorkbook wb) { - XSSFSheet sheet = wb.getSheet("Redirects"); + public void assertSpreadsheet(Workbook wb) { + Sheet sheet = wb.getSheet("Redirects"); assertNotNull(sheet); - XSSFRow row1 = sheet.getRow(1); + Row row1 = sheet.getRow(1); assertEquals("/content/one", row1.getCell(0).getStringCellValue()); assertEquals("/content/two", row1.getCell(1).getStringCellValue()); assertEquals(302, (int) row1.getCell(2).getNumericCellValue()); @@ -123,7 +125,7 @@ public void assertSpreadsheet(XSSFWorkbook wb) { assertDateEquals("22 November 1976", new Calendar.Builder().setInstant(row1.getCell(11).getDateCellValue()).build()); assertEquals("jane.doe", row1.getCell(12).getStringCellValue()); - XSSFRow row2 = sheet.getRow(2); + Row row2 = sheet.getRow(2); assertEquals("/content/three", row2.getCell(0).getStringCellValue()); assertEquals("/content/four", row2.getCell(1).getStringCellValue()); assertEquals(301, (int) row2.getCell(2).getNumericCellValue()); diff --git a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServletTest.java b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServletTest.java index d534f78aaf..01ef2fb4bd 100755 --- a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServletTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/ImportRedirectMapServletTest.java @@ -25,31 +25,37 @@ import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ValueMap; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.apache.sling.testing.mock.sling.junit.SlingContext; -import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; -import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import javax.servlet.ServletException; + import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.HashMap; import java.util.Calendar; -import java.util.Arrays; import java.util.Collections; -import java.util.Collection; import static com.adobe.acs.commons.redirects.Asserts.assertDateEquals; -import static com.adobe.acs.commons.redirects.filter.RedirectFilter.REDIRECT_RULE_RESOURCE_TYPE; +import static com.adobe.acs.commons.redirects.models.RedirectRule.CASE_INSENSITIVE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.NOTE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.SOURCE_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.TAGS_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.models.RedirectRule.TARGET_PROPERTY_NAME; +import static com.adobe.acs.commons.redirects.servlets.ExportRedirectMapServlet.CONTENT_TYPE_EXCEL; +import static com.adobe.acs.commons.redirects.servlets.ExportRedirectMapServlet.export; import static org.junit.Assert.*; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; public class ImportRedirectMapServletTest { @Rule @@ -58,13 +64,14 @@ public class ImportRedirectMapServletTest { private ImportRedirectMapServlet servlet; private String redirectStoragePath = "/conf/acs-commons/redirects"; + private Resource storageRoot; @Before public void setUp() { servlet = new ImportRedirectMapServlet(); context.request().addRequestParameter("path", redirectStoragePath); context.addModelsForClasses(RedirectRule.class); - context.build().resource(redirectStoragePath); + storageRoot = context.create().resource(redirectStoragePath); } /** @@ -101,14 +108,11 @@ public void testImportOnlyRequiredColumns() throws ServletException, IOException out.close(); byte[] excelBytes = out.toByteArray(); - MockSlingHttpServletRequest request = context.request(); - MockSlingHttpServletResponse response = context.response(); - - request.addRequestParameter("file", excelBytes, "binary/data"); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); - servlet.doPost(request, response); + // Execute + servlet.doPost(context.request(), context.response()); - Resource storageRoot = context.resourceResolver().getResource(redirectStoragePath); Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source assertEquals("number of redirects after import ", 2, rules.size()); @@ -123,7 +127,7 @@ public void testImportOnlyRequiredColumns() throws ServletException, IOException assertEquals(302, rule2.getStatusCode()); // read ImportLog from the output json and assert there were no issues - ImportLog importLog = new ObjectMapper().readValue(response.getOutputAsString(), ImportLog.class); + ImportLog importLog = new ObjectMapper().readValue(context.response().getOutputAsString(), ImportLog.class); assertEquals("ImportLog",0, importLog.getLog().size()); assertTrue(importLog.getPath(), context.resourceResolver().getResource(importLog.getPath()) != null); } @@ -179,19 +183,16 @@ public void testIgnoreInvalidRows() throws ServletException, IOException { out.close(); byte[] excelBytes = out.toByteArray(); - MockSlingHttpServletRequest request = context.request(); - MockSlingHttpServletResponse response = context.response(); - - request.addRequestParameter("file", excelBytes, "binary/data"); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); - servlet.doPost(request, response); + // Execute + servlet.doPost(context.request(), context.response()); - Resource storageRoot = context.resourceResolver().getResource(redirectStoragePath); Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source assertEquals("number of redirects after import ", 0, rules.size()); // read ImportLog from the output json and assert that every 6 input rows had issues - ImportLog importLog = new ObjectMapper().readValue(response.getOutputAsString(), ImportLog.class); + ImportLog importLog = new ObjectMapper().readValue(context.response().getOutputAsString(), ImportLog.class); List<ImportLog.Entry> logEntries = importLog.getLog(); assertEquals(6, logEntries.size()); assertTrue(importLog.getPath(), context.resourceResolver().getResource(importLog.getPath()) != null); @@ -228,19 +229,16 @@ public void testDuplicatesRedirects() throws ServletException, IOException { out.close(); byte[] excelBytes = out.toByteArray(); - MockSlingHttpServletRequest request = context.request(); - MockSlingHttpServletResponse response = context.response(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); - request.addRequestParameter("file", excelBytes, "binary/data"); + // Execute + servlet.doPost(context.request(), context.response()); - servlet.doPost(request, response); - - Resource storageRoot = context.resourceResolver().getResource(redirectStoragePath); Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source assertEquals("number of redirects after import ", 1, rules.size()); // read ImportLog from the output json and assert there were no issues - ImportLog importLog = new ObjectMapper().readValue(response.getOutputAsString(), ImportLog.class); + ImportLog importLog = new ObjectMapper().readValue(context.response().getOutputAsString(), ImportLog.class); assertEquals("ImportLog",0, importLog.getLog().size()); assertTrue(importLog.getPath(), context.resourceResolver().getResource(importLog.getPath()) != null); } @@ -288,14 +286,11 @@ public void testImportMixedColumns() throws ServletException, IOException { out.close(); byte[] excelBytes = out.toByteArray(); - MockSlingHttpServletRequest request = context.request(); - MockSlingHttpServletResponse response = context.response(); - - request.addRequestParameter("file", excelBytes, "binary/data"); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); - servlet.doPost(request, response); + // Execute + servlet.doPost(context.request(), context.response()); - Resource storageRoot = context.resourceResolver().getResource(redirectStoragePath); Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source assertEquals("number of redirects after import ", 1, rules.size()); @@ -310,178 +305,204 @@ public void testImportMixedColumns() throws ServletException, IOException { assertArrayEquals(new String[]{"redirects:tag1", "redirects:tag2"}, rule.getTagIds()); // read ImportLog from the output json and assert there were no issues - ImportLog importLog = new ObjectMapper().readValue(response.getOutputAsString(), ImportLog.class); + ImportLog importLog = new ObjectMapper().readValue(context.response().getOutputAsString(), ImportLog.class); assertEquals("ImportLog",0, importLog.getLog().size()); assertTrue(importLog.getPath(), context.resourceResolver().getResource(importLog.getPath()) != null); } - /** - * Merge redirects from spreadsheet with existing redirects in the repository - */ @Test - public void testImportMixedExistingAndSpreadsheet() throws ServletException, IOException { - // existing rules + public void testExcelImport() throws ServletException, IOException { + // Setup mock Excel file + byte[] excelBytes = createMockExcelFile(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + + assertEquals("number of redirects before import ", 0, servlet.getRules(storageRoot).size()); + + // Execute + servlet.doPost(context.request(), context.response()); + + // Verify + Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source + assertEquals("number of redirects after import ", 1, rules.size()); + assertNotNull(rules.get("/old")); + } + + @Test + public void testReplaceExistingRules() throws ServletException, IOException { + // Setup + byte[] excelBytes = createMockExcelFile(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + context.request().addRequestParameter("replace", "true"); + new RedirectResourceBuilder(context, redirectStoragePath) .setSource("/content/one") .setTarget("/content/two") .setStatusCode(302) - .setUntilDate(new Calendar.Builder().setDate(2022, 9, 9).build()) - .setNotes("note-1") - .setEvaluateURI(true) - .setContextPrefixIgnored(true) - .setCreatedBy("john.doe") - .setTagIds(new String[]{"redirects:tag3"}) - .setProperty("custom-1", "123") - .setNodeName("redirect-saved-1") .build(); + + assertEquals("number of redirects before import ", 1, servlet.getRules(storageRoot).size()); + + // Execute + servlet.doPost(context.request(), context.response()); + + // Verify + Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source + assertEquals("number of redirects after import ", 1, rules.size()); + assertNotNull(rules.get("/old")); + } + + @Test + public void testMergeExistingRules() throws ServletException, IOException { + // Setup + byte[] excelBytes = createMockExcelFile(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + new RedirectResourceBuilder(context, redirectStoragePath) - .setSource("/content/three") - .setTarget("/content/four") - .setStatusCode(301) - .setNotes("note-2") - .setModifiedBy("xyz") - .setTagIds(new String[]{"redirects:tag3"}) - .setProperty("custom-2", "345") - .setNodeName("redirect-saved-2") + .setSource("/content/one") + .setTarget("/content/two") + .setStatusCode(302) .build(); + assertEquals("number of redirects before import ", 1, servlet.getRules(storageRoot).size()); - // new rules in a spreadsheet. will be merged with the existing rules - XSSFWorkbook wb = new XSSFWorkbook(); - CellStyle dateStyle = wb.createCellStyle(); - dateStyle.setDataFormat( - wb.createDataFormat().getFormat("mmm d, yyyy")); - Sheet sheet = wb.createSheet(); - Row headerRow = sheet.createRow(0); - headerRow.createCell(0).setCellValue(ExportColumn.SOURCE.getTitle()); - headerRow.createCell(1).setCellValue(ExportColumn.TARGET.getTitle()); - headerRow.createCell(2).setCellValue(ExportColumn.STATUS_CODE.getTitle()); - headerRow.createCell(3).setCellValue(ExportColumn.OFF_TIME.getTitle()); - headerRow.createCell(4).setCellValue(ExportColumn.NOTES.getTitle()); - headerRow.createCell(5).setCellValue(ExportColumn.EVALUATE_URI.getTitle()); - headerRow.createCell(6).setCellValue(ExportColumn.IGNORE_CONTEXT_PREFIX.getTitle()); - headerRow.createCell(7).setCellValue(ExportColumn.TAGS.getTitle()); - headerRow.createCell(8).setCellValue(ExportColumn.CREATED.getTitle()); - headerRow.createCell(9).setCellValue(ExportColumn.CREATED_BY.getTitle()); - headerRow.createCell(10).setCellValue(ExportColumn.MODIFIED.getTitle()); - headerRow.createCell(11).setCellValue(ExportColumn.MODIFIED_BY.getTitle()); - headerRow.createCell(12).setCellValue(ExportColumn.ON_TIME.getTitle()); + // Execute + servlet.doPost(context.request(), context.response()); - Row row1 = sheet.createRow(1); - row1.createCell(0).setCellValue("/content/1"); - row1.createCell(1).setCellValue("/en/we-retail"); - row1.createCell(2).setCellValue(301); - row1.createCell(3).setCellValue(new Calendar.Builder().setDate(1974, 01, 16).build()); - row1.getCell(3).setCellStyle(dateStyle); - row1.createCell(4).setCellValue("note-abc"); - row1.createCell(7).setCellValue("redirects:tag1\nredirects:tag2"); - row1.createCell(12).setCellValue(new Calendar.Builder().setDate(2025, 02, 02).build()); - row1.getCell(12).setCellStyle(dateStyle); + // Verify + Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source + assertEquals("number of redirects after import ", 2, rules.size()); + assertNotNull(rules.get("/old")); + assertNotNull(rules.get("/content/one")); + } - Row row2 = sheet.createRow(2); - row2.createCell(0).setCellValue("/content/2"); - row2.createCell(1).setCellValue("/en/we-retail"); - row2.createCell(2).setCellValue(301); + @Test + public void testUpdateExistingRule() throws ServletException, IOException { + // Setup + byte[] excelBytes = createMockExcelFile(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); - Row row3 = sheet.createRow(3); - row3.createCell(0).setCellValue("/content/three"); - row3.createCell(1).setCellValue("/en/we-retail"); - row3.createCell(2).setCellValue(301); + // Setup existing rule + new RedirectResourceBuilder(context, redirectStoragePath) + .setSource("/old") + .setTarget("/content/two") + .setNotes("hello") + .setCaseInsensitive(true) + .setTagIds(new String[]{"tag:one"}) + .build(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - wb.write(out); - out.close(); - byte[] excelBytes = out.toByteArray(); + assertEquals("number of redirects before import ", 1, servlet.getRules(storageRoot).size()); - MockSlingHttpServletRequest request = context.request(); - MockSlingHttpServletResponse response = context.response(); + // Execute + servlet.doPost(context.request(), context.response()); - request.addRequestParameter("file", excelBytes, "binary/data"); + // Verify + Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source + assertEquals("number of redirects after import ", 1, rules.size()); + Resource rule = rules.get("/old"); + assertEquals("/old", rule.getValueMap().get(SOURCE_PROPERTY_NAME)); + assertEquals("/new", rule.getValueMap().get(TARGET_PROPERTY_NAME)); + assertEquals("hello", rule.getValueMap().get(NOTE_PROPERTY_NAME)); + assertEquals(true, rule.getValueMap().get(CASE_INSENSITIVE_PROPERTY_NAME)); + assertArrayEquals(new String[]{"tag:one"}, (String[])rule.getValueMap().get(TAGS_PROPERTY_NAME)); + } - servlet.doPost(request, response); + @Test + public void testShardingLargeImport() throws ServletException, IOException { + // Setup large Excel with over 1000 rules + List<RedirectRule> xlsRules = new ArrayList<>(); + for (int i = 0; i < 1500; i++) { + RedirectRule rule = spy(new RedirectRule()); + doReturn("/old-" + i).when(rule).getSource(); + doReturn("/new-" + i).when(rule).getTarget(); + doReturn(301).when(rule).getStatusCode(); + doReturn(null).when(rule).getCreatedBy(); + doReturn(null).when(rule).getModifiedBy(); + xlsRules.add(rule); + } + byte[] excelBytes = createMockExcelFile(xlsRules); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + + assertEquals("number of redirects before import ", 0, servlet.getRules(storageRoot).size()); + + // Execute + servlet.doPost(context.request(), context.response()); + + // Verify + Iterator<Resource> it = storageRoot.listChildren(); + assertEquals("shard-0", it.next().getName()); + assertEquals("shard-1", it.next().getName()); + assertFalse(it.hasNext()); - Resource storageRoot = context.resourceResolver().getResource(redirectStoragePath); Map<String, Resource> rules = servlet.getRules(storageRoot); // rules keyed by source - assertEquals("number of redirects after import ", 4, rules.size()); - - Resource res1 = rules.get("/content/one"); - assertEquals("redirect-saved-1", res1.getName()); // node name is preserved - RedirectRule rule1 = res1.adaptTo(RedirectRule.class); - assertEquals("/content/two", rule1.getTarget()); - assertDateEquals("09 October 2022", rule1.getUntilDate()); - assertEquals("note-1", rule1.getNote()); - assertEquals("john.doe", rule1.getCreatedBy()); - assertEquals("123", res1.getValueMap().get("custom-1")); - assertTrue(rule1.getEvaluateURI()); - assertTrue(rule1.getContextPrefixIgnored()); - assertArrayEquals(new String[]{"redirects:tag3"}, rule1.getTagIds()); - - Resource res2 = rules.get("/content/three"); - assertEquals("redirect-saved-2", res2.getName()); // node name is preserved - RedirectRule rule2 = res2.adaptTo(RedirectRule.class); - assertEquals("/en/we-retail", rule2.getTarget()); - assertEquals(301, rule2.getStatusCode()); - assertFalse(rule2.getEvaluateURI()); - assertFalse(rule2.getContextPrefixIgnored()); - assertEquals("xyz", rule2.getModifiedBy()); - assertEquals("345", res2.getValueMap().get("custom-2")); - - RedirectRule rule3 = rules.get("/content/1").adaptTo(RedirectRule.class); - assertEquals("/en/we-retail", rule3.getTarget()); - assertDateEquals("16 February 1974", rule3.getUntilDate()); - assertEquals("note-abc", rule3.getNote()); - assertFalse(rule3.getEvaluateURI()); - assertFalse(rule3.getContextPrefixIgnored()); - assertArrayEquals(new String[]{"redirects:tag1", "redirects:tag2"}, rule3.getTagIds()); - assertDateEquals("02 March 2025", rule3.getEffectiveFrom()); - - RedirectRule rule4 = rules.get("/content/2").adaptTo(RedirectRule.class); - assertEquals("/en/we-retail", rule4.getTarget()); - assertEquals(null, rule4.getUntilDate()); + assertEquals("number of redirects after import ", 1500, rules.size()); + } - // read ImportLog from the output json and assert there were no issues - ImportLog importLog = new ObjectMapper().readValue(response.getOutputAsString(), ImportLog.class); - assertEquals("ImportLog",0, importLog.getLog().size()); + @Test + public void testImportWithValidation() throws ServletException, IOException { + // Setup CSV with invalid rule + RedirectRule rule = spy(new RedirectRule()); + doReturn("").when(rule).getSource(); + doReturn("/new").when(rule).getTarget(); + doReturn(301).when(rule).getStatusCode(); + doReturn(null).when(rule).getCreatedBy(); + doReturn(null).when(rule).getModifiedBy(); + + byte[] excelBytes = createMockExcelFile(Collections.singletonList(rule)); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + + // Execute + servlet.doPost(context.request(), context.response()); + + // Verify + String jsonResponse = context.response().getOutputAsString(); + ImportLog auditLog = new ObjectMapper().readValue(jsonResponse, ImportLog.class); + ImportLog.Entry entry = auditLog.getLog().get(0); // Should contain validation warning + assertEquals("A2", entry.getCell()); + assertEquals(ImportLog.Level.WARN, entry.getLevel()); + assertEquals("Cell A is required and should contain redirect source", entry.getMsg()); } @Test - public void testUpdate() throws IOException { - Map<String, Object> rule1 = new HashMap<>(); - rule1.put("sling:resourceType", REDIRECT_RULE_RESOURCE_TYPE); - rule1.put(RedirectRule.SOURCE_PROPERTY_NAME, "/a1"); - rule1.put(RedirectRule.TARGET_PROPERTY_NAME, "/b1"); - rule1.put(RedirectRule.STATUS_CODE_PROPERTY_NAME, 301); - - Map<String, Object> rule2 = new HashMap<>(); - rule2.put("sling:resourceType", REDIRECT_RULE_RESOURCE_TYPE); - rule2.put(RedirectRule.SOURCE_PROPERTY_NAME, "/a2"); - rule2.put(RedirectRule.TARGET_PROPERTY_NAME, "/b2"); - rule2.put(RedirectRule.STATUS_CODE_PROPERTY_NAME, 302); - rule2.put(RedirectRule.UNTIL_DATE_PROPERTY_NAME, Calendar.getInstance()); - rule2.put(RedirectRule.NOTE_PROPERTY_NAME, "note"); - rule2.put(RedirectRule.EVALUATE_URI_PROPERTY_NAME, true); - rule2.put(RedirectRule.CONTEXT_PREFIX_IGNORED_PROPERTY_NAME, true); - - Collection<Map<String, Object>> rules = Arrays.asList(rule1, rule2); - - Resource root = context.resourceResolver().getResource(redirectStoragePath); - servlet.update(root, rules, Collections.emptyMap()); - - Map<String, Resource> redirects = servlet.getRules(root); - ValueMap vm1 = redirects.get(rule1.get(RedirectRule.SOURCE_PROPERTY_NAME)).getValueMap(); - - assertEquals(vm1.get(RedirectRule.SOURCE_PROPERTY_NAME), rule1.get(RedirectRule.SOURCE_PROPERTY_NAME)); - assertEquals(vm1.get(RedirectRule.TARGET_PROPERTY_NAME), rule1.get(RedirectRule.TARGET_PROPERTY_NAME)); - assertFalse(vm1.containsKey(RedirectRule.UNTIL_DATE_PROPERTY_NAME)); - assertFalse(vm1.containsKey(RedirectRule.NOTE_PROPERTY_NAME)); - assertFalse(vm1.containsKey(RedirectRule.EVALUATE_URI_PROPERTY_NAME)); - assertFalse(vm1.containsKey(RedirectRule.CONTEXT_PREFIX_IGNORED_PROPERTY_NAME)); - - ValueMap vm2 = redirects.get(rule2.get(RedirectRule.SOURCE_PROPERTY_NAME)).getValueMap(); - assertEquals(vm2.get(RedirectRule.SOURCE_PROPERTY_NAME), rule2.get(RedirectRule.SOURCE_PROPERTY_NAME)); - assertEquals(vm2.get(RedirectRule.TARGET_PROPERTY_NAME), rule2.get(RedirectRule.TARGET_PROPERTY_NAME)); - assertEquals(vm2.get(RedirectRule.NOTE_PROPERTY_NAME), rule2.get(RedirectRule.NOTE_PROPERTY_NAME)); - assertEquals(vm2.get(RedirectRule.EVALUATE_URI_PROPERTY_NAME), rule2.get(RedirectRule.EVALUATE_URI_PROPERTY_NAME)); - assertEquals(vm2.get(RedirectRule.CONTEXT_PREFIX_IGNORED_PROPERTY_NAME), rule2.get(RedirectRule.CONTEXT_PREFIX_IGNORED_PROPERTY_NAME)); + public void testAuditLogCreation() throws ServletException, IOException { + // Setup + byte[] excelBytes = createMockExcelFile(); + setupFileUpload(excelBytes, CONTENT_TYPE_EXCEL ); + + // Execute + servlet.doPost(context.request(), context.response()); + + // Verify + String jsonResponse = context.response().getOutputAsString(); + String auditLogPath = new ObjectMapper().readValue(jsonResponse, ImportLog.class).getPath(); + Resource auditResource = context.resourceResolver().getResource(auditLogPath); + assertNotNull(auditResource); // Should contain audit log path + } + + private void setupFileUpload(byte[] content, String contentType) throws IOException { + context.request().addRequestParameter("file", content, contentType); + } + + private byte[] createMockExcelFile() throws IOException { + RedirectRule rule = spy(new RedirectRule()); + doReturn("/old").when(rule).getSource(); + doReturn("/new").when(rule).getTarget(); + doReturn(301).when(rule).getStatusCode(); + doReturn(null).when(rule).getCreatedBy(); + doReturn(null).when(rule).getModifiedBy(); + + Workbook wb = export(Collections.singletonList(rule)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + wb.write(out); + out.close(); + return out.toByteArray(); } + private byte[] createMockExcelFile(List<RedirectRule> rules) throws IOException { + Workbook wb = export(rules); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + wb.write(out); + out.close(); + return out.toByteArray(); + } } diff --git a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServletTest.java b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServletTest.java index fd7bf8a1c1..3189e95bfb 100755 --- a/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServletTest.java +++ b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServletTest.java @@ -23,6 +23,7 @@ import org.apache.http.entity.ContentType; import org.apache.sling.api.resource.PersistenceException; import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ValueMap; import org.apache.sling.testing.mock.sling.ResourceResolverType; import org.apache.sling.testing.mock.sling.junit.SlingContext; import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; @@ -45,7 +46,7 @@ public class RewriteMapServletTest { public SlingContext context = new SlingContext(ResourceResolverType.RESOURCERESOLVER_MOCK); private RewriteMapServlet servlet; - private final String redirectStoragePath = "/conf/acs-commons/redirects"; + private final String redirectStoragePath = "/conf/acs-commons/settings/redirects"; @Before public void setUp() throws PersistenceException { @@ -73,13 +74,13 @@ public void setUp() throws PersistenceException { .build(); Resource redirects = context.resourceResolver().getResource(redirectStoragePath); - context.request().setResource(redirects); + context.request().setResource(redirects); servlet = new RewriteMapServlet(); } @Test - public void testGet() throws ServletException, IOException { + public void testDoGetWithNoSelector() throws ServletException, IOException { MockSlingHttpServletRequest request = context.request(); MockSlingHttpServletResponse response = context.response(); @@ -101,7 +102,7 @@ public void testGet() throws ServletException, IOException { } @Test - public void test301Selector() throws ServletException, IOException { + public void testDoGetWithStatusCodeSelector() throws ServletException, IOException { MockSlingHttpServletRequest request = context.request(); MockSlingHttpServletResponse response = context.response(); @@ -117,21 +118,13 @@ public void test301Selector() throws ServletException, IOException { assertEquals("/content/four", rule1[1]); } - @Test - public void test302Selector() throws ServletException, IOException { + @Test(expected = NumberFormatException.class) + public void testDoGetWithInvalidStatusCodeSelector() throws ServletException, IOException { MockSlingHttpServletRequest request = context.request(); MockSlingHttpServletResponse response = context.response(); - context.requestPathInfo().setSelectorString("302"); + context.requestPathInfo().setSelectorString("NA"); servlet.doGet(request, response); - assertEquals(ContentType.TEXT_PLAIN.getMimeType(), response.getContentType()); - String[] lines = response.getOutputAsString().split("\n"); - assertEquals(3, lines.length); // header + notes + 1st rule - assertEquals("# 302 Redirects", lines[0]); - - String[] rule1 = lines[2].split(" "); - assertEquals("/content/one", rule1[0]); - assertEquals("/content/two", rule1[1]); } } \ No newline at end of file diff --git a/oakpal-checks/pom.xml b/oakpal-checks/pom.xml index d7d6e1ed6c..1e278ed9a0 100644 --- a/oakpal-checks/pom.xml +++ b/oakpal-checks/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/pom.xml b/pom.xml index 02806ca03c..e7b58249ca 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> <packaging>pom</packaging> <name>ACS AEM Commons - Reactor Project</name> diff --git a/ui.apps/pom.xml b/ui.apps/pom.xml index 440729d063..8adc2a85b1 100644 --- a/ui.apps/pom.xml +++ b/ui.apps/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/clientlibs/app.js b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/clientlibs/app.js index 1779e67bae..551caaac66 100755 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/clientlibs/app.js +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/clientlibs/app.js @@ -276,13 +276,18 @@ var $form = $(this).closest("form"); var data = new FormData($form[0]); + var ui = $(window).adaptTo("foundation-ui"); + ui.wait(); $.ajax({ url: $form.attr("action"), type: "POST", data: new FormData($form[0]), processData: false, contentType: false + }).fail(function (response) { + ui.clearWait(); }).done(function (response) { + ui.clearWait(); var isErr = response.log.length; if (response.log.length) { var maxItems = 10; diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/manage-redirects.html b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/manage-redirects.html index 939bff6492..37b5f3298d 100755 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/manage-redirects.html +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/manage-redirects/manage-redirects.html @@ -164,8 +164,8 @@ <coral-panel class="coral-Well"> <section> <h2 class="coral-Heading coral-Heading--2">Export Redirect Map</h2> - <p>Export combined redirect map into Excel spreadsheet. You can edit it offline and upload the redirect configurations using the Import option.</p> - <form action="${resource.path}.export.html" method="get" class="coral-Form--aligned"> + <p>Export redirects to Excel for offline editing. Use the Import option to upload modified redirect configurations.</p> + <form action="${resource.path}.export.csv" method="get" class="coral-Form--aligned"> <input type="hidden" name="path" value="${context.redirectResource.path}" /> <div class="coral-Form-fieldwrapper"> <button class="coral-Button coral-Button--primary cq-dialog-download" icon="fileExcel" is="coral-button" variant="primary">Export Redirect Map</button> @@ -176,15 +176,20 @@ <h2 class="coral-Heading coral-Heading--2">Export Redirect Map</h2> <coral-panel class="coral-Well"> <section> <h2 class="coral-Heading coral-Heading--2">Import Redirect Map</h2> - <p>The redirect map file will be combined with the redirects configured in AEM to create the final set of redirects. This file should be a spreadsheet file with the first column containing the source path, the second column containing the redirect destination and the third column containing the redirect status code.</p> + <p>Excel Spreadsheet must include three columns: source path, destination path, and HTTP status code.</p> <form action="${resource.path}.import.html" method="post" class="coral-Form--aligned" enctype="multipart/form-data" > <input type="hidden" name="path" value="${context.redirectResource.path}" /> <div class="coral-Form-fieldwrapper"> - <label class="coral-Form-fieldlabel" id="label-vertical-inputgroup-2"> Excel Spreadsheet With Redirects * </label> + <label class="coral-Form-fieldlabel" id="label-vertical-inputgroup-2"> Excel Spreadsheet or CSV file with Redirects * </label> <div class="coral-InputGroup coral-Form-field"> - <input is="coral-Textfield" class="coral-InputGroup-input coral3-Textfield" id="acs-redirect-import-ctrl" type="file" name="./redirects.redirectmap.xlsx" accept=".xlsx" /> + <input is="coral-Textfield" class="coral-InputGroup-input coral3-Textfield" id="acs-redirect-import-ctrl" type="file" name="./redirects.redirectmap.xlsx" accept=".xlsx,.csv" /> </div> </div> + <div class="coral-Form-fieldwrapper"> + <coral-checkbox name="replace" value="true" class="coral-Form-field"> + Replace Mode. When enabled, overwrites existing redirects in AEM. When disabled, combines with AEM's existing redirects. + </coral-checkbox> + </div> <div class="coral-Form-fieldwrapper"> <button class="coral-Button acs-redirects-form-import" icon="fileExcel" is="coral-button" variant="primary">Import Redirect Map</button> </div> diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/redirect-manager/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/redirect-manager/.content.xml index 8642061800..e80e638155 100755 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/redirect-manager/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/content/redirect-manager/.content.xml @@ -376,6 +376,12 @@ name="./fulltextSearchEnabled" uncheckedValue="{Boolean}false" value="{Boolean}true"/> + <pageSize + jcr:primaryType="nt:unstructured" + sling:resourceType="granite/ui/components/coral/foundation/form/numberfield" + emptyText="100" + fieldLabel="Number of redirects to display per page" + name="./pageSize"/> <cache-control-headers jcr:primaryType="nt:unstructured" jcr:title="Default Cache Control Headers" diff --git a/ui.config/pom.xml b/ui.config/pom.xml index 8c41b25eaf..4e9356ac5c 100644 --- a/ui.config/pom.xml +++ b/ui.config/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== --> diff --git a/ui.content/pom.xml b/ui.content/pom.xml index 1f164ff795..542fe048c9 100644 --- a/ui.content/pom.xml +++ b/ui.content/pom.xml @@ -25,7 +25,7 @@ <parent> <groupId>com.adobe.acs</groupId> <artifactId>acs-aem-commons</artifactId> - <version>6.10.1-SNAPSHOT</version> + <version>6.11.0-SNAPSHOT</version> </parent> <!-- ====================================================================== -->