diff --git a/CHANGELOG.md b/CHANGELOG.md
index be5eb6ed27..c36cadb265 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com)
-
## Unreleased ([details][unreleased changes details])
-- #3480 - Sites Copy Publish URLs
+
+- #3484 - Redirect Manager: A servlet to export redirects to a TXT file to use with pipeline-free redirects
+- #3480 - AEM Sites Copy Publish URLs
### Fixed
- #3479 - Fixed Configurations Model for Redirect Manager after change in "redirect" resource as "sling:Folder"
diff --git a/all/pom.xml b/all/pom.xml
index bd627dd784..1308ebaad6 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/bundle-cloud/pom.xml b/bundle-cloud/pom.xml
index 1b60a6d39b..01c9534ac2 100644
--- a/bundle-cloud/pom.xml
+++ b/bundle-cloud/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/bundle-onprem/pom.xml b/bundle-onprem/pom.xml
index 5604477d36..a6db518ee4 100644
--- a/bundle-onprem/pom.xml
+++ b/bundle-onprem/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/bundle/pom.xml b/bundle/pom.xml
index 23ea706103..56be916ea2 100644
--- a/bundle/pom.xml
+++ b/bundle/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
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
new file mode 100644
index 0000000000..b1a1ca345e
--- /dev/null
+++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServlet.java
@@ -0,0 +1,83 @@
+/*-
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2013 - 2024 Adobe
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.adobe.acs.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.servlets.SlingSafeMethodsServlet;
+import org.osgi.service.component.annotations.Component;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collection;
+
+import static com.adobe.acs.commons.redirects.servlets.CreateRedirectConfigurationServlet.REDIRECTS_RESOURCE_PATH;
+
+
+/**
+ * Servlet for generating an Apache RewriteMap text file to use with
+ * he Pipeline-free URL Redirects feature in AEM as a Cloud Service
+ *
+ * Usage: http://localhost:4502/conf/my-site/settings/redirects.txt
+ * To filter by status code: http://localhost:4502/conf/my-site/settings/redirects.301.txt
+ *
+ * See https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/implementing/content-delivery/pipeline-free-url-redirects
+ *
+ */
+@Component(service = Servlet.class, property = {
+ "sling.servlet.methods=GET",
+ "sling.servlet.extensions=txt",
+ "sling.servlet.resourceTypes=" + REDIRECTS_RESOURCE_PATH
+})
+public class RewriteMapServlet extends SlingSafeMethodsServlet {
+
+ private static final long serialVersionUID = -3564475196678277711L;
+
+ @Override
+ @SuppressWarnings("java:S3457")
+ protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+ throws ServletException, IOException {
+ response.setContentType(ContentType.TEXT_PLAIN.getMimeType());
+
+ String[] selectors = request.getRequestPathInfo().getSelectors();
+ int statusCode = 0;
+ if(selectors != null && selectors.length > 0) {
+ statusCode = Integer.parseInt(selectors[0]);
+ }
+ Collection rules = RedirectFilter.getRules(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) {
+ continue;
+ }
+ String note = rule.getNote();
+ if(note != null && !note.isEmpty()) {
+ out.printf("# %s\n", note);
+ }
+ out.printf("%s %s\n", rule.getSource(), rule.getTarget());
+ }
+ }
+}
diff --git a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/package-info.java b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/package-info.java
index 29b37a5407..7185a8e113 100755
--- a/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/package-info.java
+++ b/bundle/src/main/java/com/adobe/acs/commons/redirects/servlets/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("1.1.0")
+@org.osgi.annotation.versioning.Version("1.2.0")
package com.adobe.acs.commons.redirects.servlets;
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
new file mode 100755
index 0000000000..fd7bf8a1c1
--- /dev/null
+++ b/bundle/src/test/java/com/adobe/acs/commons/redirects/servlets/RewriteMapServletTest.java
@@ -0,0 +1,137 @@
+/*-
+ * #%L
+ * ACS AEM Commons Bundle
+ * %%
+ * Copyright (C) 2013 - 2024 Adobe
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+package com.adobe.acs.commons.redirects.servlets;
+
+import com.adobe.acs.commons.redirects.RedirectResourceBuilder;
+import org.apache.http.entity.ContentType;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+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.IOException;
+import java.util.Calendar;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Yegor Kozlov
+ */
+public class RewriteMapServletTest {
+ @Rule
+ public SlingContext context = new SlingContext(ResourceResolverType.RESOURCERESOLVER_MOCK);
+
+ private RewriteMapServlet servlet;
+ private final String redirectStoragePath = "/conf/acs-commons/redirects";
+
+ @Before
+ public void setUp() throws PersistenceException {
+ new RedirectResourceBuilder(context, redirectStoragePath)
+ .setSource("/content/one")
+ .setTarget("/content/two")
+ .setStatusCode(302)
+ .setUntilDate(new Calendar.Builder().setDate(2022, 9, 9).build())
+ .setEffectiveFrom(new Calendar.Builder().setDate(2025, 2, 2).build())
+ .setNotes("note-1")
+ .setEvaluateURI(true)
+ .setContextPrefixIgnored(true)
+ .setTagIds(new String[]{"redirects:tag1"})
+ .setCreatedBy("john.doe")
+ .setModifiedBy("jane.doe")
+ .setCreated(new Calendar.Builder().setDate(1974, 1, 16).build())
+ .setModified(new Calendar.Builder().setDate(1976, 10, 22).build())
+ .build();
+ new RedirectResourceBuilder(context, redirectStoragePath)
+ .setSource("/content/three")
+ .setTarget("/content/four")
+ .setStatusCode(301)
+ .setTagIds(new String[]{"redirects:tag2"})
+ .setModifiedBy("john.doe")
+ .build();
+
+ Resource redirects = context.resourceResolver().getResource(redirectStoragePath);
+ context.request().setResource(redirects);
+ servlet = new RewriteMapServlet();
+ }
+
+
+ @Test
+ public void testGet() throws ServletException, IOException {
+ MockSlingHttpServletRequest request = context.request();
+ MockSlingHttpServletResponse response = context.response();
+
+ servlet.doGet(request, response);
+
+ assertEquals(ContentType.TEXT_PLAIN.getMimeType(), response.getContentType());
+ String[] lines = response.getOutputAsString().split("\n");
+ assertEquals(4, lines.length); // header + 2 rules
+ assertEquals("# All Redirects", lines[0]);
+ assertEquals("# note-1", lines[1]);
+
+ String[] rule1 = lines[2].split(" ");
+ assertEquals("/content/one", rule1[0]);
+ assertEquals("/content/two", rule1[1]);
+
+ String[] rule2 = lines[3].split(" ");
+ assertEquals("/content/three", rule2[0]);
+ assertEquals("/content/four", rule2[1]);
+ }
+
+ @Test
+ public void test301Selector() throws ServletException, IOException {
+ MockSlingHttpServletRequest request = context.request();
+ MockSlingHttpServletResponse response = context.response();
+
+ context.requestPathInfo().setSelectorString("301");
+ servlet.doGet(request, response);
+
+ assertEquals(ContentType.TEXT_PLAIN.getMimeType(), response.getContentType());
+ String[] lines = response.getOutputAsString().split("\n");
+ assertEquals(2, lines.length); // header + 1 rule
+ assertEquals("# 301 Redirects", lines[0]);
+ String[] rule1 = lines[1].split(" ");
+ assertEquals("/content/three", rule1[0]);
+ assertEquals("/content/four", rule1[1]);
+ }
+
+ @Test
+ public void test302Selector() throws ServletException, IOException {
+ MockSlingHttpServletRequest request = context.request();
+ MockSlingHttpServletResponse response = context.response();
+
+ context.requestPathInfo().setSelectorString("302");
+ 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 af51347817..049977ac3f 100644
--- a/oakpal-checks/pom.xml
+++ b/oakpal-checks/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/pom.xml b/pom.xml
index 9fe8eca330..81f7b1e502 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
pom
ACS AEM Commons - Reactor Project
diff --git a/ui.apps/pom.xml b/ui.apps/pom.xml
index 455b2730da..9e69ad8891 100644
--- a/ui.apps/pom.xml
+++ b/ui.apps/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/ui.config/pom.xml b/ui.config/pom.xml
index 3b3aa08ea3..1e70526e75 100644
--- a/ui.config/pom.xml
+++ b/ui.config/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT
diff --git a/ui.content/pom.xml b/ui.content/pom.xml
index 8188e9cb80..cf2fb0e777 100644
--- a/ui.content/pom.xml
+++ b/ui.content/pom.xml
@@ -25,7 +25,7 @@
com.adobe.acs
acs-aem-commons
- 6.9.7-SNAPSHOT
+ 6.10.0-SNAPSHOT