Skip to content

Commit b27d6e5

Browse files
authored
Merge pull request #34 from evolvedbinary/6.x.x/feature/url-rewrite-redirect-type
[6.x.x] Allow the redirect type to be specified in the URL Rewrite Controller
2 parents dc8d821 + 9cd10a6 commit b27d6e5

File tree

4 files changed

+265
-12
lines changed

4 files changed

+265
-12
lines changed

exist-core/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@
690690
<include>src/main/java/org/exist/dom/memtree/reference/ElementReferenceImpl.java</include>
691691
<include>src/main/java/org/exist/dom/memtree/reference/ProcessingInstructionReferenceImpl.java</include>
692692
<include>src/main/java/org/exist/dom/memtree/reference/TextReferenceImpl.java</include>
693+
<include>src/test/java/org/exist/http/urlrewrite/RedirectTest.java</include>
693694
<include>src/main/java/org/exist/util/ByteOrderMark.java</include>
694695
<include>src/main/java/org/exist/util/JREUtil.java</include>
695696
<include>src/main/java/org/exist/util/OSUtil.java</include>
@@ -1088,6 +1089,7 @@
10881089
<exclude>src/main/java/org/exist/http/urlrewrite/ModuleCall.java</exclude>
10891090
<exclude>src/main/java/org/exist/http/urlrewrite/PathForward.java</exclude>
10901091
<exclude>src/main/java/org/exist/http/urlrewrite/Redirect.java</exclude>
1092+
<exclude>src/test/java/org/exist/http/urlrewrite/RedirectTest.java</exclude>
10911093
<exclude>src/main/java/org/exist/http/urlrewrite/RewriteConfig.java</exclude>
10921094
<exclude>src/main/java/org/exist/indexing/Index.java</exclude>
10931095
<include>src/main/java/org/exist/indexing/IndexController.java</include>

exist-core/src/main/java/org/exist/http/urlrewrite/Redirect.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,19 @@
5151
import javax.servlet.ServletException;
5252
import javax.servlet.http.HttpServletRequest;
5353
import javax.servlet.http.HttpServletResponse;
54+
55+
import javax.annotation.Nullable;
5456
import java.io.IOException;
5557

58+
import static org.exist.util.StringUtil.isNullOrEmpty;
59+
5660
public class Redirect extends URLRewrite {
5761

62+
private @Nullable RedirectType redirectType;
63+
5864
public Redirect(final Element config, final String uri) throws ServletException {
5965
super(config, uri);
66+
this.redirectType = parseRedirectType(config.getAttribute("type"));
6067
final String redirectTo = config.getAttribute("url");
6168
if (redirectTo.isEmpty()) {
6269
throw new ServletException("<exist:redirect> needs an attribute 'url'.");
@@ -71,16 +78,66 @@ public Redirect(final Element config, final String uri) throws ServletException
7178

7279
public Redirect(final Redirect other) {
7380
super(other);
81+
this.redirectType = other.redirectType;
7482
}
7583

7684
@Override
7785
public void doRewrite(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
78-
setHeaders(new HttpResponseWrapper(response));
79-
response.sendRedirect(target);
86+
if (redirectType == null) {
87+
redirectType = "GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod()) ? RedirectType.Found : RedirectType.SeeOther;
88+
}
89+
90+
final HttpResponseWrapper responseWrapper = new HttpResponseWrapper(response);
91+
setHeaders(responseWrapper);
92+
responseWrapper.setStatusCode(redirectType.httpStatusCode);
93+
responseWrapper.setHeader("Location", target);
94+
95+
// commit the response
96+
responseWrapper.flushBuffer();
8097
}
8198

8299
@Override
83100
protected URLRewrite copy() {
84101
return new Redirect(this);
85102
}
103+
104+
private static @Nullable RedirectType parseRedirectType(@Nullable final String strRedirectType) throws ServletException {
105+
// first, if no value use the default
106+
if (isNullOrEmpty(strRedirectType)) {
107+
return null;
108+
}
109+
110+
// second, try to parse by number
111+
try {
112+
final int intRedirectType = Integer.valueOf(strRedirectType);
113+
for (final RedirectType redirectType : RedirectType.values()) {
114+
if (redirectType.httpStatusCode == intRedirectType) {
115+
return redirectType;
116+
}
117+
}
118+
} catch (final NumberFormatException e) {
119+
// ignore - no op
120+
}
121+
122+
// third, try to parse by name
123+
try {
124+
return RedirectType.valueOf(strRedirectType);
125+
} catch (final IllegalArgumentException e) {
126+
throw new ServletException("<exist:redirect type=\"" + strRedirectType + "\" is unsupported.");
127+
}
128+
}
129+
130+
enum RedirectType {
131+
MovedPermanently(301),
132+
Found(302),
133+
SeeOther(303),
134+
TemporaryRedirect(307),
135+
PermanentRedirect(308);
136+
137+
public final int httpStatusCode;
138+
139+
RedirectType(final int httpStatusCode) {
140+
this.httpStatusCode = httpStatusCode;
141+
}
142+
}
86143
}

exist-core/src/test/java/org/exist/http/AbstractHttpTest.java

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
9595
}
9696

9797
/**
98-
* Execute a function with a HTTP Client.
98+
* Execute a function with an HTTP Client.
9999
*
100100
* @param <T> the return type of the <code>fn</code> function.
101101
* @param fn the function which accepts the HTTP Client.
@@ -105,16 +105,40 @@ protected static String getAppsUri(final ExistWebServer existWebServer) {
105105
* @throws IOException if an I/O error occurs
106106
*/
107107
protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException> fn) throws IOException {
108-
try (final CloseableHttpClient client = HttpClientBuilder
109-
.create()
110-
.disableAutomaticRetries()
111-
.build()) {
108+
return withHttpClient(false, fn);
109+
}
110+
111+
/**
112+
* Execute a function with an HTTP Client.
113+
*
114+
* @param <T> the return type of the <code>fn</code> function.
115+
* @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default).
116+
* @param fn the function which accepts the HTTP Client.
117+
*
118+
* @return the result of the <code>fn</code> function.
119+
*
120+
* @throws IOException if an I/O error occurs
121+
*/
122+
protected static <T> T withHttpClient(final boolean disableRedirectHandling, final FunctionE<HttpClient, T, IOException> fn) throws IOException {
123+
try (final CloseableHttpClient client = buildHttpClient(disableRedirectHandling)) {
112124
return fn.apply(client);
113125
}
114126
}
115127

128+
private static CloseableHttpClient buildHttpClient(final boolean disableRedirectHandling) {
129+
HttpClientBuilder builder = HttpClientBuilder
130+
.create()
131+
.disableAutomaticRetries();
132+
133+
if (disableRedirectHandling) {
134+
builder = builder.disableRedirectHandling();
135+
}
136+
137+
return builder.build();
138+
}
139+
116140
/**
117-
* Execute a function with a HTTP Executor.
141+
* Execute a function with an HTTP Executor.
118142
*
119143
* @param <T> the return type of the <code>fn</code> function.
120144
* @param existWebServer the Web Server.
@@ -125,11 +149,27 @@ protected static <T> T withHttpClient(final FunctionE<HttpClient, T, IOException
125149
* @throws IOException if an I/O error occurs
126150
*/
127151
protected static <T> T withHttpExecutor(final ExistWebServer existWebServer, final FunctionE<Executor, T, IOException> fn) throws IOException {
128-
return withHttpClient(client -> {
152+
return withHttpExecutor(existWebServer, false, fn);
153+
}
154+
155+
/**
156+
* Execute a function with an HTTP Executor.
157+
*
158+
* @param <T> the return type of the <code>fn</code> function.
159+
* @param existWebServer the Web Server.
160+
* @param disableRedirectHandling true to disable redirect handling, false to leave it enabled (default).
161+
* @param fn the function which accepts the HTTP Executor.
162+
*
163+
* @return the result of the <code>fn</code> function.
164+
*
165+
* @throws IOException if an I/O error occurs
166+
*/
167+
protected static <T> T withHttpExecutor(final ExistWebServer existWebServer, final boolean disableRedirectHandling, final FunctionE<Executor, T, IOException> fn) throws IOException {
168+
return withHttpClient(disableRedirectHandling, client -> {
129169
final Executor executor = Executor
130-
.newInstance(client)
131-
.auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
132-
.authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
170+
.newInstance(client)
171+
.auth(TestUtils.ADMIN_DB_USER, TestUtils.ADMIN_DB_PWD)
172+
.authPreemptive(new HttpHost("localhost", existWebServer.getPort()));
133173
return fn.apply(executor);
134174
});
135175
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Elemental
3+
* Copyright (C) 2024, Evolved Binary Ltd
4+
*
5+
6+
* https://www.evolvedbinary.com | https://www.elemental.xyz
7+
*
8+
* This library is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU Lesser General Public
10+
* License as published by the Free Software Foundation; version 2.1.
11+
*
12+
* This library is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15+
* Lesser General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public
18+
* License along with this library; if not, write to the Free Software
19+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20+
*/
21+
package org.exist.http.urlrewrite;
22+
23+
import com.evolvedbinary.j8fu.tuple.Tuple2;
24+
import org.apache.http.HttpResponse;
25+
import org.apache.http.HttpStatus;
26+
import org.apache.http.client.fluent.Request;
27+
import org.apache.http.entity.ContentType;
28+
import org.exist.http.AbstractHttpTest;
29+
import org.exist.test.ExistWebServer;
30+
import org.exist.xmldb.XmldbURI;
31+
import org.junit.BeforeClass;
32+
import org.junit.ClassRule;
33+
import org.junit.Test;
34+
35+
import javax.annotation.Nullable;
36+
import java.io.IOException;
37+
import java.util.function.Function;
38+
39+
import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple;
40+
import static org.exist.http.urlrewrite.XQueryURLRewrite.XQUERY_CONTROLLER_FILENAME;
41+
import static org.junit.Assert.assertEquals;
42+
43+
/**
44+
* @author <a href="mailto:[email protected]">Adam Retter</a>
45+
*/
46+
public class RedirectTest extends AbstractHttpTest {
47+
48+
private static final XmldbURI TEST_COLLECTION_NAME = XmldbURI.create("redirect-test");
49+
private static final XmldbURI TEST_COLLECTION = XmldbURI.create("/db/apps").append(TEST_COLLECTION_NAME);
50+
51+
private static final String TEST_CONTROLLER =
52+
"xquery version \"1.0\";\n" +
53+
"declare namespace exist = \"http://exist.sourceforge.net/NS/exist\";\n" +
54+
"declare namespace request = \"http://exist-db.org/xquery/request\";\n" +
55+
"let $redirect-type := request:get-parameter(\"redirect-type\", ())\n" +
56+
"return\n" +
57+
" element exist:dispatch {\n" +
58+
" element exist:redirect {\n" +
59+
" attribute url { \"http://elsewhere.dom\" },\n" +
60+
" $redirect-type ! attribute type { . }\n" +
61+
" }\n" +
62+
" }\n";
63+
64+
@ClassRule
65+
public static final ExistWebServer EXIST_WEB_SERVER = new ExistWebServer(true, false, true, true, false);
66+
67+
@Test
68+
public void defaultForGetIsFound() throws IOException {
69+
testRedirect(Redirect.RedirectType.Found, null, Request::Get);
70+
}
71+
72+
@Test
73+
public void defaultForHeadIsFound() throws IOException {
74+
testRedirect(Redirect.RedirectType.Found, null, Request::Head);
75+
}
76+
77+
@Test
78+
public void defaultForPostIsSeeOther() throws IOException {
79+
testRedirect(Redirect.RedirectType.SeeOther, null, Request::Post);
80+
}
81+
82+
@Test
83+
public void defaultForPatchIsSeeOther() throws IOException {
84+
testRedirect(Redirect.RedirectType.SeeOther, null, Request::Patch);
85+
}
86+
87+
@Test
88+
public void defaultForPutIsSeeOther() throws IOException {
89+
testRedirect(Redirect.RedirectType.SeeOther, null, Request::Put);
90+
}
91+
92+
@Test
93+
public void defaultForDeleteIsSeeOther() throws IOException {
94+
testRedirect(Redirect.RedirectType.SeeOther, null, Request::Delete);
95+
}
96+
97+
@Test
98+
public void movedPermanently() throws IOException {
99+
testGetSendRedirect(Redirect.RedirectType.MovedPermanently);
100+
}
101+
102+
@Test
103+
public void found() throws IOException {
104+
testGetSendRedirect(Redirect.RedirectType.Found);
105+
}
106+
107+
@Test
108+
public void seeOther() throws IOException {
109+
testGetSendRedirect(Redirect.RedirectType.SeeOther);
110+
}
111+
112+
@Test
113+
public void temporaryRedirect() throws IOException {
114+
testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect);
115+
}
116+
117+
@Test
118+
public void permanentRedirect() throws IOException {
119+
testGetSendRedirect(Redirect.RedirectType.TemporaryRedirect);
120+
}
121+
122+
private void testGetSendRedirect(final Redirect.RedirectType redirectType) throws IOException {
123+
testGetRedirect(redirectType, redirectType);
124+
}
125+
126+
private void testGetRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType) throws IOException {
127+
testRedirect(expectedRedirectTypeResponse, sendRedirectType, Request::Get);
128+
}
129+
130+
private void testRedirect(final Redirect.RedirectType expectedRedirectTypeResponse, @Nullable final Redirect.RedirectType sendRedirectType, final Function<String, Request> requestFn) throws IOException {
131+
final String uri = getAppsUri(EXIST_WEB_SERVER) + "/" + TEST_COLLECTION_NAME.append("anything") + (sendRedirectType != null ? "?redirect-type=" + sendRedirectType.httpStatusCode : "");
132+
final Request request = requestFn.apply(uri);
133+
final Tuple2<Integer, String> redirectStatusCodeAndLocation = withHttpExecutor(EXIST_WEB_SERVER, true, executor -> {
134+
final HttpResponse response = executor.execute(request).returnResponse();
135+
return Tuple(response.getStatusLine().getStatusCode(), response.getFirstHeader("Location").getValue());
136+
});
137+
assertEquals(expectedRedirectTypeResponse.httpStatusCode, redirectStatusCodeAndLocation._1.intValue());
138+
assertEquals("http://elsewhere.dom", redirectStatusCodeAndLocation._2);
139+
}
140+
141+
142+
@BeforeClass
143+
public static void setup() throws IOException {
144+
final Request request = Request
145+
.Put(getRestUri(EXIST_WEB_SERVER) + TEST_COLLECTION + "/" + XQUERY_CONTROLLER_FILENAME)
146+
.bodyString(TEST_CONTROLLER, ContentType.create("application/xquery"));
147+
148+
final int statusCode = withHttpExecutor(EXIST_WEB_SERVER, executor ->
149+
executor.execute(request).returnResponse().getStatusLine().getStatusCode()
150+
);
151+
152+
assertEquals(HttpStatus.SC_CREATED, statusCode);
153+
}
154+
}

0 commit comments

Comments
 (0)