Jetty 12 - Introduce PathMappingsHandler (#8748)

* Introduce PathMappingsHandler
This commit is contained in:
Joakim Erdfelt 2022-10-25 10:04:33 -05:00 committed by GitHub
parent 05e1722045
commit 8eb10b2d74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 418 additions and 0 deletions

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);
}
}
}