diff --git a/exist-core/pom.xml b/exist-core/pom.xml
index 3e7e580829..76b9ee317d 100644
--- a/exist-core/pom.xml
+++ b/exist-core/pom.xml
@@ -739,6 +739,7 @@
src/main/java/org/exist/dom/memtree/reference/ElementReferenceImpl.java
src/main/java/org/exist/dom/memtree/reference/ProcessingInstructionReferenceImpl.java
src/main/java/org/exist/dom/memtree/reference/TextReferenceImpl.java
+ src/test/java/org/exist/http/urlrewrite/RedirectTest.java
src/main/java/org/exist/util/ByteOrderMark.java
src/main/java/org/exist/util/JREUtil.java
src/main/java/org/exist/util/OSUtil.java
@@ -1087,6 +1088,7 @@
src/main/java/org/exist/http/urlrewrite/ModuleCall.java
src/main/java/org/exist/http/urlrewrite/PathForward.java
src/main/java/org/exist/http/urlrewrite/Redirect.java
+ src/test/java/org/exist/http/urlrewrite/RedirectTest.java
src/main/java/org/exist/http/urlrewrite/RewriteConfig.java
src/main/java/org/exist/indexing/Index.java
src/main/java/org/exist/indexing/IndexManager.java
diff --git a/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java b/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java
index 533500a884..e063325afa 100644
--- a/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java
+++ b/exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java
@@ -51,12 +51,19 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+
+import javax.annotation.Nullable;
import java.io.IOException;
+import static org.exist.util.StringUtil.isNullOrEmpty;
+
public class Redirect extends URLRewrite {
+ private @Nullable RedirectType redirectType;
+
public Redirect(final Element config, final String uri) throws ServletException {
super(config, uri);
+ this.redirectType = parseRedirectType(config.getAttribute("type"));
final String redirectTo = config.getAttribute("url");
if (redirectTo.isEmpty()) {
throw new ServletException(" needs an attribute 'url'.");
@@ -71,16 +78,66 @@ public Redirect(final Element config, final String uri) throws ServletException
public Redirect(final Redirect other) {
super(other);
+ this.redirectType = other.redirectType;
}
@Override
public void doRewrite(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
- setHeaders(new HttpResponseWrapper(response));
- response.sendRedirect(target);
+ if (redirectType == null) {
+ redirectType = "GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod()) ? RedirectType.Found : RedirectType.SeeOther;
+ }
+
+ final HttpResponseWrapper responseWrapper = new HttpResponseWrapper(response);
+ setHeaders(responseWrapper);
+ responseWrapper.setStatusCode(redirectType.httpStatusCode);
+ responseWrapper.setHeader("Location", target);
+
+ // commit the response
+ responseWrapper.flushBuffer();
}
@Override
protected URLRewrite copy() {
return new Redirect(this);
}
+
+ private static @Nullable RedirectType parseRedirectType(@Nullable final String strRedirectType) throws ServletException {
+ // first, if no value use the default
+ if (isNullOrEmpty(strRedirectType)) {
+ return null;
+ }
+
+ // second, try to parse by number
+ try {
+ final int intRedirectType = Integer.valueOf(strRedirectType);
+ for (final RedirectType redirectType : RedirectType.values()) {
+ if (redirectType.httpStatusCode == intRedirectType) {
+ return redirectType;
+ }
+ }
+ } catch (final NumberFormatException e) {
+ // ignore - no op
+ }
+
+ // third, try to parse by name
+ try {
+ return RedirectType.valueOf(strRedirectType);
+ } catch (final IllegalArgumentException e) {
+ throw new ServletException(" the return type of the fn
function.
* @param fn the function which accepts the HTTP Client.
@@ -105,16 +105,40 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
* @throws IOException if an I/O error occurs
*/
protected static T withHttpClient(final FunctionE fn) throws IOException {
- try (final CloseableHttpClient client = HttpClientBuilder
- .create()
- .disableAutomaticRetries()
- .build()) {
+ return withHttpClient(false, fn);
+ }
+
+ /**
+ * Execute a function with an HTTP Client.
+ *
+ * @param the return type of the fn
function.
+ * @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default).
+ * @param fn the function which accepts the HTTP Client.
+ *
+ * @return the result of the fn
function.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ protected static T withHttpClient(final boolean disableRedirectHandling, final FunctionE fn) throws IOException {
+ try (final CloseableHttpClient client = buildHttpClient(disableRedirectHandling)) {
return fn.apply(client);
}
}
+ private static CloseableHttpClient buildHttpClient(final boolean disableRedirectHandling) {
+ HttpClientBuilder builder = HttpClientBuilder
+ .create()
+ .disableAutomaticRetries();
+
+ if (disableRedirectHandling) {
+ builder = builder.disableRedirectHandling();
+ }
+
+ return builder.build();
+ }
+
/**
- * Execute a function with a HTTP Executor.
+ * Execute a function with an HTTP Executor.
*
* @param the return type of the fn
function.
* @param existWebServer the Web Server.
@@ -125,11 +149,27 @@ protected static T withHttpClient(final FunctionE T withHttpExecutor(final ExistWebServer existWebServer, final FunctionE fn) throws IOException {
- return withHttpClient(client -> {
+ return withHttpExecutor(existWebServer, false, fn);
+ }
+
+ /**
+ * Execute a function with an HTTP Executor.
+ *
+ * @param the return type of the fn
function.
+ * @param existWebServer the Web Server.
+ * @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default).
+ * @param fn the function which accepts the HTTP Executor.
+ *
+ * @return the result of the fn
function.
+ *
+ * @throws IOException if an I/O error occurs
+ */
+ protected static T withHttpExecutor(final ExistWebServer existWebServer, final boolean disableRedirectHandling, final FunctionE fn) throws IOException {
+ return withHttpClient(disableRedirectHandling, client -> {
final Executor executor = Executor
- .newInstance(client)
- .auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
- .authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
+ .newInstance(client)
+ .auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
+ .authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
return fn.apply(executor);
});
}
diff --git a/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java b/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java
new file mode 100644
index 0000000000..e1ebe3a79a
--- /dev/null
+++ b/exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java
@@ -0,0 +1,154 @@
+/*
+ * Elemental
+ * Copyright (C) 2024, Evolved Binary Ltd
+ *
+ * admin@evolvedbinary.com
+ * https://www.evolvedbinary.com | https://www.elemental.xyz
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; version 2.1.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+package org.exist.http.urlrewrite;
+
+import com.evolvedbinary.j8fu.tuple.Tuple2;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.fluent.Request;
+import org.apache.http.entity.ContentType;
+import org.exist.http.AbstractHttpTest;
+import org.exist.test.ExistWebServer;
+import org.exist.xmldb.XmldbURI;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.function.Function;
+
+import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
+import static org.exist.http.urlrewrite.XQueryURLRewrite.XQUERY_CONTROLLER_FILENAME;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Adam Retter
+ */
+public class RedirectTest extends AbstractHttpTest {
+
+ private static final XmldbURI TEST_COLLECTION_NAME = XmldbURI.create("redirect-test");
+ private static final XmldbURI TEST_COLLECTION = XmldbURI.create("/db/apps").append(TEST_COLLECTION_NAME);
+
+ private static final String TEST_CONTROLLER =
+ "xquery version \"1.0\";\n" +
+ "declare namespace exist = \"http://exist.sourceforge.net/NS/exist\";\n" +
+ "declare namespace request = \"http://exist-db.org/xquery/request\";\n" +
+ "let $redirect-type := request:get-parameter(\"redirect-type\", ())\n" +
+ "return\n" +
+ " element exist:dispatch {\n" +
+ " element exist:redirect {\n" +
+ " attribute url { \"http://elsewhere.dom\" },\n" +
+ " $redirect-type ! attribute type { . }\n" +
+ " }\n" +
+ " }\n";
+
+ @ClassRule
+ public static final ExistWebServer EXIST_WEB_SERVER = new ExistWebServer(true, false, true, true, false);
+
+ @Test
+ public void defaultForGetIsFound() throws IOException {
+ testRedirect(Redirect.RedirectType.Found, null, Request::Get);
+ }
+
+ @Test
+ public void defaultForHeadIsFound() throws IOException {
+ testRedirect(Redirect.RedirectType.Found, null, Request::Head);
+ }
+
+ @Test
+ public void defaultForPostIsSeeOther() throws IOException {
+ testRedirect(Redirect.RedirectType.SeeOther, null, Request::Post);
+ }
+
+ @Test
+ public void defaultForPatchIsSeeOther() throws IOException {
+ testRedirect(Redirect.RedirectType.SeeOther, null, Request::Patch);
+ }
+
+ @Test
+ public void defaultForPutIsSeeOther() throws IOException {
+ testRedirect(Redirect.RedirectType.SeeOther, null, Request::Put);
+ }
+
+ @Test
+ public void defaultForDeleteIsSeeOther() throws IOException {
+ testRedirect(Redirect.RedirectType.SeeOther, null, Request::Delete);
+ }
+
+ @Test
+ public void movedPermanently() throws IOException {
+ testGetSendRedirect(Redirect.RedirectType.MovedPermanently);
+ }
+
+ @Test
+ public void found() throws IOException {
+ testGetSendRedirect(Redirect.RedirectType.Found);
+ }
+
+ @Test
+ public void seeOther() throws IOException {
+ testGetSendRedirect(Redirect.RedirectType.SeeOther);
+ }
+
+ @Test
+ public void temporaryRedirect() throws IOException {
+ testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect);
+ }
+
+ @Test
+ public void permanentRedirect() throws IOException {
+ testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect);
+ }
+
+ private void testGetSendRedirect(final Redirect.RedirectType redirectType) throws IOException {
+ testGetRedirect(redirectType, redirectType);
+ }
+
+ private void testGetRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType) throws IOException {
+ testRedirect(expectedRedirectTypeResponse, sendRedirectType, Request::Get);
+ }
+
+ private void testRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType, final Function requestFn) throws IOException {
+ final String uri = getAppsUri(EXIST_WEB_SERVER) + "/" + TEST_COLLECTION_NAME.append("anything") + (sendRedirectType != null ? "?redirect-type=" + sendRedirectType.httpStatusCode : "");
+ final Request request = requestFn.apply(uri);
+ final Tuple2 redirectStatusCodeAndLocation = withHttpExecutor(EXIST_WEB_SERVER, true, executor -> {
+ final HttpResponse response = executor.execute(request).returnResponse();
+ return Tuple(response.getStatusLine().getStatusCode(), response.getFirstHeader("Location").getValue());
+ });
+ assertEquals(expectedRedirectTypeResponse.httpStatusCode, redirectStatusCodeAndLocation._1.intValue());
+ assertEquals("http://elsewhere.dom", redirectStatusCodeAndLocation._2);
+ }
+
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ final Request request = Request
+ .Put(getRestUri(EXIST_WEB_SERVER) + TEST_COLLECTION + "/" + XQUERY_CONTROLLER_FILENAME)
+ .bodyString(TEST_CONTROLLER, ContentType.create("application/xquery"));
+
+ final int statusCode = withHttpExecutor(EXIST_WEB_SERVER, executor ->
+ executor.execute(request).returnResponse().getStatusLine().getStatusCode()
+ );
+
+ assertEquals(HttpStatus.SC_CREATED, statusCode);
+ }
+}