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>
 
     <!-- ====================================================================== -->