Skip to content

[7.x.x] Allow the redirect type to be specified in the URL Rewrite Controller #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions exist-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@
<include>src/main/java/org/exist/dom/memtree/reference/ElementReferenceImpl.java</include>
<include>src/main/java/org/exist/dom/memtree/reference/ProcessingInstructionReferenceImpl.java</include>
<include>src/main/java/org/exist/dom/memtree/reference/TextReferenceImpl.java</include>
<include>src/test/java/org/exist/http/urlrewrite/RedirectTest.java</include>
<include>src/main/java/org/exist/util/ByteOrderMark.java</include>
<include>src/main/java/org/exist/util/JREUtil.java</include>
<include>src/main/java/org/exist/util/OSUtil.java</include>
Expand Down Expand Up @@ -1087,6 +1088,7 @@
<exclude>src/main/java/org/exist/http/urlrewrite/ModuleCall.java</exclude>
<exclude>src/main/java/org/exist/http/urlrewrite/PathForward.java</exclude>
<exclude>src/main/java/org/exist/http/urlrewrite/Redirect.java</exclude>
<exclude>src/test/java/org/exist/http/urlrewrite/RedirectTest.java</exclude>
<exclude>src/main/java/org/exist/http/urlrewrite/RewriteConfig.java</exclude>
<exclude>src/main/java/org/exist/indexing/Index.java</exclude>
<exclude>src/main/java/org/exist/indexing/IndexManager.java</exclude>
Expand Down
61 changes: 59 additions & 2 deletions exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("<exist:redirect> needs an attribute 'url'.");
Expand All @@ -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("<exist:redirect type=\"" + strRedirectType + "\" is unsupported.");
}
}

enum RedirectType {
MovedPermanently(301),
Found(302),
SeeOther(303),
TemporaryRedirect(307),
PermanentRedirect(308);

public final int httpStatusCode;

RedirectType(final int httpStatusCode) {
this.httpStatusCode = httpStatusCode;
}
}
}
60 changes: 50 additions & 10 deletions exist-core/src/test/java/org/exist/http/AbstractHttpTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
}

/**
* Execute a function with a HTTP Client.
* Execute a function with an HTTP Client.
*
* @param <T> the return type of the <code>fn</code> function.
* @param fn the function which accepts the HTTP Client.
Expand All @@ -105,16 +105,40 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
* @throws IOException if an I/O error occurs
*/
protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException> fn) throws IOException {
try (final CloseableHttpClient client = HttpClientBuilder
.create()
.disableAutomaticRetries()
.build()) {
return withHttpClient(false, fn);
}

/**
* Execute a function with an HTTP Client.
*
* @param <T> the return type of the <code>fn</code> 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 <code>fn</code> function.
*
* @throws IOException if an I/O error occurs
*/
protected static <T> T withHttpClient(final boolean disableRedirectHandling, final FunctionE<HttpClient, T, IOException> 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 <T> the return type of the <code>fn</code> function.
* @param existWebServer the Web Server.
Expand All @@ -125,11 +149,27 @@ protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException
* @throws IOException if an I/O error occurs
*/
protected static <T> T withHttpExecutor(final ExistWebServer existWebServer, final FunctionE<Executor, T, IOException> fn) throws IOException {
return withHttpClient(client -> {
return withHttpExecutor(existWebServer, false, fn);
}

/**
* Execute a function with an HTTP Executor.
*
* @param <T> the return type of the <code>fn</code> 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 <code>fn</code> function.
*
* @throws IOException if an I/O error occurs
*/
protected static <T> T withHttpExecutor(final ExistWebServer existWebServer, final boolean disableRedirectHandling, final FunctionE<Executor, T, IOException> 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);
});
}
Expand Down
154 changes: 154 additions & 0 deletions exist-core/src/test/java/org/exist/http/urlrewrite/RedirectTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Elemental
* Copyright (C) 2024, Evolved Binary Ltd
*
* [email protected]
* 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 <a href="mailto:[email protected]">Adam Retter</a>
*/
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<String, Request> 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<Integer, String> 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);
}
}