Jetty 12 - Introduce PathMappingsHandler (#8748)
* Introduce PathMappingsHandler
This commit is contained in:
parent
05e1722045
commit
8eb10b2d74
|
@ -21,6 +21,7 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.util.Index;
|
||||
import org.eclipse.jetty.util.annotation.ManagedAttribute;
|
||||
|
@ -88,6 +89,11 @@ public class PathMappings<E> implements Iterable<MappedResource<E>>, Dumpable
|
|||
_suffixMap.clear();
|
||||
}
|
||||
|
||||
public Stream<MappedResource<E>> streamResources()
|
||||
{
|
||||
return _mappings.stream();
|
||||
}
|
||||
|
||||
public void removeIf(Predicate<MappedResource<E>> predicate)
|
||||
{
|
||||
_mappings.removeIf(predicate);
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.eclipse.jetty.http.pathmap.MappedResource;
|
||||
import org.eclipse.jetty.http.pathmap.MatchedResource;
|
||||
import org.eclipse.jetty.http.pathmap.PathMappings;
|
||||
import org.eclipse.jetty.http.pathmap.PathSpec;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.util.component.Dumpable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A Handler that delegates to other handlers through a configured {@link PathMappings}.
|
||||
*/
|
||||
|
||||
public class PathMappingsHandler extends Handler.AbstractContainer
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PathMappingsHandler.class);
|
||||
|
||||
private final PathMappings<Handler> mappings = new PathMappings<>();
|
||||
|
||||
@Override
|
||||
public void addHandler(Handler handler)
|
||||
{
|
||||
throw new UnsupportedOperationException("Arbitrary addHandler() not supported, use addMapping() instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHandler(Supplier<Handler> supplier)
|
||||
{
|
||||
throw new UnsupportedOperationException("Arbitrary addHandler() not supported, use addMapping() instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Handler> getHandlers()
|
||||
{
|
||||
return mappings.streamResources().map(MappedResource::getResource).toList();
|
||||
}
|
||||
|
||||
public void addMapping(PathSpec pathSpec, Handler handler)
|
||||
{
|
||||
if (isStarted())
|
||||
throw new IllegalStateException("Cannot add mapping: " + this);
|
||||
|
||||
// check that self isn't present
|
||||
if (handler == this || handler instanceof Handler.Container container && container.getDescendants().contains(this))
|
||||
throw new IllegalStateException("Unable to addHandler of self: " + handler);
|
||||
|
||||
// check existing mappings
|
||||
for (MappedResource<Handler> entry : mappings)
|
||||
{
|
||||
Handler entryHandler = entry.getResource();
|
||||
|
||||
if (entryHandler == this ||
|
||||
entryHandler == handler ||
|
||||
(entryHandler instanceof Handler.Container container && container.getDescendants().contains(this)))
|
||||
throw new IllegalStateException("addMapping loop detected: " + handler);
|
||||
}
|
||||
|
||||
mappings.put(pathSpec, handler);
|
||||
addBean(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dump(Appendable out, String indent) throws IOException
|
||||
{
|
||||
Dumpable.dumpObjects(out, indent, this, mappings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Request.Processor handle(Request request) throws Exception
|
||||
{
|
||||
String pathInContext = request.getPathInContext();
|
||||
MatchedResource<Handler> matchedResource = mappings.getMatched(pathInContext);
|
||||
if (matchedResource == null)
|
||||
{
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("No match on pathInContext of {}", pathInContext);
|
||||
return null;
|
||||
}
|
||||
if (LOG.isDebugEnabled())
|
||||
LOG.debug("Matched pathInContext of {} to {} -> {}", pathInContext, matchedResource.getPathSpec(), matchedResource.getResource());
|
||||
return matchedResource.getResource().handle(request);
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ import static org.hamcrest.Matchers.nullValue;
|
|||
import static org.hamcrest.Matchers.sameInstance;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ContextHandlerTest
|
||||
|
@ -586,6 +587,31 @@ public class ContextHandlerTest
|
|||
assertThat(result.get(), equalTo("OK"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetHandlerLoopSelf()
|
||||
{
|
||||
ContextHandler contextHandlerA = new ContextHandler();
|
||||
assertThrows(IllegalStateException.class, () -> contextHandlerA.setHandler(contextHandlerA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetHandlerLoopDeepWrapper()
|
||||
{
|
||||
ContextHandler contextHandlerA = new ContextHandler();
|
||||
Handler.Wrapper handlerWrapper = new Handler.Wrapper();
|
||||
contextHandlerA.setHandler(handlerWrapper);
|
||||
assertThrows(IllegalStateException.class, () -> handlerWrapper.setHandler(contextHandlerA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddHandlerLoopDeep()
|
||||
{
|
||||
ContextHandler contextHandlerA = new ContextHandler();
|
||||
Handler.Collection handlerCollection = new Handler.Collection();
|
||||
contextHandlerA.setHandler(handlerCollection);
|
||||
assertThrows(IllegalStateException.class, () -> handlerCollection.addHandler(contextHandlerA));
|
||||
}
|
||||
|
||||
private static class ScopeListener implements ContextHandler.ContextScopeListener
|
||||
{
|
||||
private static final Request NULL = new Request.Wrapper(null);
|
||||
|
|
|
@ -0,0 +1,283 @@
|
|||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package org.eclipse.jetty.server.handler;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpTester;
|
||||
import org.eclipse.jetty.http.pathmap.ServletPathSpec;
|
||||
import org.eclipse.jetty.server.Handler;
|
||||
import org.eclipse.jetty.server.LocalConnector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Response;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class PathMappingsHandlerTest
|
||||
{
|
||||
private Server server;
|
||||
private LocalConnector connector;
|
||||
|
||||
public void startServer(Handler handler) throws Exception
|
||||
{
|
||||
server = new Server();
|
||||
connector = new LocalConnector(server);
|
||||
server.addConnector(connector);
|
||||
|
||||
server.addHandler(handler);
|
||||
server.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void stopServer()
|
||||
{
|
||||
LifeCycle.stop(server);
|
||||
}
|
||||
|
||||
public HttpTester.Response executeRequest(String rawRequest) throws Exception
|
||||
{
|
||||
String rawResponse = connector.getResponse(rawRequest);
|
||||
return HttpTester.parseResponse(rawResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test where there are no mappings, and no wrapper.
|
||||
*/
|
||||
@Test
|
||||
public void testEmpty() throws Exception
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
contextHandler.setHandler(pathMappingsHandler);
|
||||
|
||||
startServer(contextHandler);
|
||||
|
||||
HttpTester.Response response = executeRequest("""
|
||||
GET / HTTP/1.1\r
|
||||
Host: local\r
|
||||
Connection: close\r
|
||||
|
||||
""");
|
||||
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test where there is only a single mapping, and no wrapper.
|
||||
*/
|
||||
@Test
|
||||
public void testOnlyMappingSuffix() throws Exception
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new SimpleHandler("PhpExample Hit"));
|
||||
contextHandler.setHandler(pathMappingsHandler);
|
||||
|
||||
startServer(contextHandler);
|
||||
|
||||
HttpTester.Response response = executeRequest("""
|
||||
GET /hello HTTP/1.1\r
|
||||
Host: local\r
|
||||
Connection: close\r
|
||||
|
||||
""");
|
||||
assertEquals(HttpStatus.NOT_FOUND_404, response.getStatus());
|
||||
|
||||
response = executeRequest("""
|
||||
GET /hello.php HTTP/1.1\r
|
||||
Host: local\r
|
||||
Connection: close\r
|
||||
|
||||
""");
|
||||
assertEquals(HttpStatus.OK_200, response.getStatus());
|
||||
assertEquals("PhpExample Hit", response.getContent());
|
||||
}
|
||||
|
||||
public static Stream<Arguments> severalMappingsInput()
|
||||
{
|
||||
return Stream.of(
|
||||
Arguments.of("/hello", HttpStatus.OK_200, "FakeResourceHandler Hit"),
|
||||
Arguments.of("/index.html", HttpStatus.OK_200, "FakeSpecificStaticHandler Hit"),
|
||||
Arguments.of("/index.php", HttpStatus.OK_200, "PhpHandler Hit"),
|
||||
Arguments.of("/config.php", HttpStatus.OK_200, "PhpHandler Hit"),
|
||||
Arguments.of("/css/main.css", HttpStatus.OK_200, "FakeResourceHandler Hit")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test where there are a few mappings, with a root mapping, and no wrapper.
|
||||
* This means the wrapper would not ever be hit, as all inputs would match at
|
||||
* least 1 mapping.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("severalMappingsInput")
|
||||
public void testSeveralMappingAndNoWrapper(String requestPath, int expectedStatus, String expectedResponseBody) throws Exception
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/"), new SimpleHandler("FakeResourceHandler Hit"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/index.html"), new SimpleHandler("FakeSpecificStaticHandler Hit"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new SimpleHandler("PhpHandler Hit"));
|
||||
contextHandler.setHandler(pathMappingsHandler);
|
||||
|
||||
startServer(contextHandler);
|
||||
|
||||
HttpTester.Response response = executeRequest("""
|
||||
GET %s HTTP/1.1\r
|
||||
Host: local\r
|
||||
Connection: close\r
|
||||
|
||||
""".formatted(requestPath));
|
||||
assertEquals(expectedStatus, response.getStatus());
|
||||
assertEquals(expectedResponseBody, response.getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDump() throws Exception
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/"), new SimpleHandler("FakeResourceHandler Hit"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/index.html"), new SimpleHandler("FakeSpecificStaticHandler Hit"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new SimpleHandler("PhpHandler Hit"));
|
||||
contextHandler.setHandler(pathMappingsHandler);
|
||||
|
||||
startServer(contextHandler);
|
||||
|
||||
String dump = contextHandler.dump();
|
||||
assertThat(dump, containsString("FakeResourceHandler"));
|
||||
assertThat(dump, containsString("FakeSpecificStaticHandler"));
|
||||
assertThat(dump, containsString("PhpHandler"));
|
||||
assertThat(dump, containsString("PathMappings[size=3]"));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDescendantsSimple()
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/"), new SimpleHandler("default"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/index.html"), new SimpleHandler("specific"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), new SimpleHandler("php"));
|
||||
|
||||
List<String> actualHandlers = pathMappingsHandler.getDescendants().stream().map(Objects::toString).toList();
|
||||
|
||||
String[] expectedHandlers = {
|
||||
"SimpleHandler[msg=\"default\"]",
|
||||
"SimpleHandler[msg=\"specific\"]",
|
||||
"SimpleHandler[msg=\"php\"]"
|
||||
};
|
||||
assertThat(actualHandlers, containsInAnyOrder(expectedHandlers));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDescendantsDeep()
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
contextHandler.setContextPath("/");
|
||||
|
||||
Handler.Collection handlerCollection = new Handler.Collection();
|
||||
handlerCollection.addHandler(new SimpleHandler("phpIndex"));
|
||||
Handler.Wrapper handlerWrapper = new Handler.Wrapper(new SimpleHandler("other"));
|
||||
handlerCollection.addHandler(handlerWrapper);
|
||||
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/"), new SimpleHandler("default"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("/index.html"), new SimpleHandler("specific"));
|
||||
pathMappingsHandler.addMapping(new ServletPathSpec("*.php"), handlerCollection);
|
||||
|
||||
List<String> actualHandlers = pathMappingsHandler.getDescendants().stream().map(Objects::toString).toList();
|
||||
|
||||
String[] expectedHandlers = {
|
||||
"SimpleHandler[msg=\"default\"]",
|
||||
"SimpleHandler[msg=\"specific\"]",
|
||||
handlerCollection.toString(),
|
||||
handlerWrapper.toString(),
|
||||
"SimpleHandler[msg=\"phpIndex\"]",
|
||||
"SimpleHandler[msg=\"other\"]"
|
||||
};
|
||||
assertThat(actualHandlers, containsInAnyOrder(expectedHandlers));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLoopSelf()
|
||||
{
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
assertThrows(IllegalStateException.class, () -> pathMappingsHandler.addMapping(new ServletPathSpec("/self"), pathMappingsHandler));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddLoopContext()
|
||||
{
|
||||
ContextHandler contextHandler = new ContextHandler();
|
||||
PathMappingsHandler pathMappingsHandler = new PathMappingsHandler();
|
||||
contextHandler.setHandler(pathMappingsHandler);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> pathMappingsHandler.addMapping(new ServletPathSpec("/loop"), contextHandler));
|
||||
}
|
||||
|
||||
private static class SimpleHandler extends Handler.Processor
|
||||
{
|
||||
private final String message;
|
||||
|
||||
public SimpleHandler(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Request request, Response response, Callback callback)
|
||||
{
|
||||
assertTrue(isStarted());
|
||||
response.setStatus(HttpStatus.OK_200);
|
||||
response.getHeaders().put(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8");
|
||||
response.write(true, BufferUtil.toBuffer(message, StandardCharsets.UTF_8), callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.format("%s[msg=\"%s\"]", SimpleHandler.class.getSimpleName(), message);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue