From 5362467e6798815ddf566ae42abea44f383b96bd Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 06:30:40 -0600 Subject: [PATCH 01/10] Issue #5539 - Proper StatisticsServlet output format via content negotiation Signed-off-by: Joakim Erdfelt --- jetty-servlet/pom.xml | 5 + .../jetty/servlet/StatisticsServlet.java | 610 +++++++++++++----- .../jetty/servlet/StatisticsServletTest.java | 202 +++++- 3 files changed, 646 insertions(+), 171 deletions(-) diff --git a/jetty-servlet/pom.xml b/jetty-servlet/pom.xml index 00abe4bbbb06..2217267cde0f 100644 --- a/jetty-servlet/pom.xml +++ b/jetty-servlet/pom.xml @@ -41,6 +41,11 @@ jetty-security ${project.version} + + org.eclipse.jetty + jetty-util-ajax + ${project.version} + org.eclipse.jetty jetty-jmx diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index 55fd67963aeb..3c6f325dd8b4 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -19,29 +19,41 @@ package org.eclipse.jetty.servlet; import java.io.IOException; -import java.io.PrintWriter; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.QuotedQualityCSV; import org.eclipse.jetty.io.ConnectionStatistics; import org.eclipse.jetty.server.AbstractConnector; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.ConnectorStatistics; -import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; -import org.eclipse.jetty.util.component.Container; +import org.eclipse.jetty.util.StringUtil; +import org.eclipse.jetty.util.ajax.JSON; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; +import static java.nio.charset.StandardCharsets.UTF_8; + public class StatisticsServlet extends HttpServlet { private static final Logger LOG = Log.getLogger(StatisticsServlet.class); @@ -49,7 +61,7 @@ public class StatisticsServlet extends HttpServlet boolean _restrictToLocalhost = true; // defaults to true private StatisticsHandler _statsHandler; private MemoryMXBean _memoryBean; - private Connector[] _connectors; + private List _connectors; @Override public void init() throws ServletException @@ -58,20 +70,16 @@ public void init() throws ServletException ContextHandler.Context scontext = (ContextHandler.Context)context; Server server = scontext.getContextHandler().getServer(); - Handler handler = server.getChildHandlerByClass(StatisticsHandler.class); + _statsHandler = server.getChildHandlerByClass(StatisticsHandler.class); - if (handler != null) - { - _statsHandler = (StatisticsHandler)handler; - } - else + if (_statsHandler == null) { LOG.warn("Statistics Handler not installed!"); return; } _memoryBean = ManagementFactory.getMemoryMXBean(); - _connectors = server.getConnectors(); + _connectors = Arrays.asList(server.getConnectors()); if (getInitParameter("restrictToLocalhost") != null) { @@ -80,47 +88,150 @@ public void init() throws ServletException } @Override - public void doPost(HttpServletRequest sreq, HttpServletResponse sres) throws ServletException, IOException + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - doGet(sreq, sres); + doGet(request, response); } @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if (_statsHandler == null) { LOG.warn("Statistics Handler not installed!"); - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return; } if (_restrictToLocalhost) { - if (!isLoopbackAddress(req.getRemoteAddr())) + if (!isLoopbackAddress(request.getRemoteAddr())) { - resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } - if (Boolean.parseBoolean(req.getParameter("statsReset"))) + if (Boolean.parseBoolean(request.getParameter("statsReset"))) { + response.setStatus(HttpServletResponse.SC_OK); _statsHandler.statsReset(); return; } - String wantXml = req.getParameter("xml"); - if (wantXml == null) - wantXml = req.getParameter("XML"); + List acceptable = getOrderedAcceptableMimeTypes(request); + + for (String mimeType : acceptable) + { + switch (mimeType) + { + case "application/json": + writeJsonResponse(response); + return; + case "text/xml": + writeXmlResponse(response); + return; + case "text/html": + writeHtmlResponse(response); + return; + case "text/plain": + writeTextResponse(response); + return; + case "*/*": + String wantXml = request.getParameter("xml"); + if (wantXml == null) + wantXml = request.getParameter("XML"); + + if (Boolean.parseBoolean(wantXml)) + { + writeXmlResponse(response); + } + else + { + writeTextResponse(response); + } + return; + default: + if (LOG.isDebugEnabled()) + { + LOG.debug("Ignoring unrecognized mime-type {}", mimeType); + } + break; + } + } + // None of the listed `Accept` mime-types were found. + response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE); + } + + private void writeTextResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/plain"); + CharSequence text = generateResponse(new TextProducer()); + response.getWriter().print(text.toString()); + } + + private void writeHtmlResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/html"); + Writer htmlWriter = new OutputStreamWriter(response.getOutputStream(), UTF_8); + htmlWriter.append(""); + htmlWriter.append(this.getClass().getSimpleName()); + htmlWriter.append("\n"); + CharSequence html = generateResponse(new HtmlProducer()); + htmlWriter.append(html.toString()); + htmlWriter.append("\n\n"); + htmlWriter.flush(); + } + + private void writeXmlResponse(HttpServletResponse response) throws IOException + { + response.setCharacterEncoding("utf-8"); + response.setContentType("text/xml"); + CharSequence xml = generateResponse(new XmlProducer()); + response.getWriter().print(xml.toString()); + } - if (Boolean.parseBoolean(wantXml)) + private void writeJsonResponse(HttpServletResponse response) throws IOException + { + // We intentionally don't put "UTF-8" into the response headers + // as the rules for application/json state that it should never be + // present on the HTTP Content-Type header. + // It is also true that the application/json mime-type is always UTF-8. + response.setContentType("application/json"); + CharSequence json = generateResponse(new JsonProducer()); + Writer jsonWriter = new OutputStreamWriter(response.getOutputStream(), UTF_8); + jsonWriter.append(json); + jsonWriter.flush(); + } + + private List getOrderedAcceptableMimeTypes(HttpServletRequest request) + { + QuotedQualityCSV values = null; + Enumeration enumAccept = request.getHeaders("Accept"); + if (enumAccept != null) { - sendXmlResponse(resp); + while (enumAccept.hasMoreElements()) + { + String value = enumAccept.nextElement(); + if (StringUtil.isNotBlank(value)) + { + if (values == null) + { + values = new QuotedQualityCSV(QuotedQualityCSV.MOST_SPECIFIC_MIME_ORDERING); + } + values.addValue(value); + } + } } - else + + if (values != null) { - sendTextResponse(resp); + return values.getValues(); } + + // No accept specified, return that we allow ALL mime types + return Collections.singletonList("*/*"); } private boolean isLoopbackAddress(String address) @@ -137,177 +248,362 @@ private boolean isLoopbackAddress(String address) } } - private void sendXmlResponse(HttpServletResponse response) throws IOException + private CharSequence generateResponse(OutputProducer outputProducer) { - StringBuilder sb = new StringBuilder(); - - sb.append("\n"); - - sb.append(" \n"); - sb.append(" ").append(_statsHandler.getStatsOnMs()).append("\n"); - - sb.append(" ").append(_statsHandler.getRequests()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestsActive()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestsActiveMax()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeTotal()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeMean()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeMax()).append("\n"); - sb.append(" ").append(_statsHandler.getRequestTimeStdDev()).append("\n"); - - sb.append(" ").append(_statsHandler.getDispatched()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedActive()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedActiveMax()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeTotal()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeMean()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeMax()).append("\n"); - sb.append(" ").append(_statsHandler.getDispatchedTimeStdDev()).append("\n"); - - sb.append(" ").append(_statsHandler.getAsyncRequests()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncRequestsWaiting()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncRequestsWaitingMax()).append("\n"); - sb.append(" ").append(_statsHandler.getAsyncDispatches()).append("\n"); - sb.append(" ").append(_statsHandler.getExpires()).append("\n"); - sb.append(" \n"); - - sb.append(" \n"); - sb.append(" ").append(_statsHandler.getResponses1xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses2xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses3xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses4xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponses5xx()).append("\n"); - sb.append(" ").append(_statsHandler.getResponsesBytesTotal()).append("\n"); - sb.append(" \n"); - - sb.append(" \n"); - for (Connector connector : _connectors) - { - sb.append(" \n"); - sb.append(" ").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("\n"); - sb.append(" \n"); - for (String protocol : connector.getProtocols()) - { - sb.append(" ").append(protocol).append("\n"); - } - sb.append(" \n"); + Map top = new HashMap<>(); + + // requests + Map requests = new HashMap<>(); + requests.put("statsOnMs", _statsHandler.getStatsOnMs()); + + requests.put("requests", _statsHandler.getRequests()); + + requests.put("requestsActive", _statsHandler.getRequestsActive()); + requests.put("requestsActiveMax", _statsHandler.getRequestsActiveMax()); + requests.put("requestsTimeTotal", _statsHandler.getRequestTimeTotal()); + requests.put("requestsTimeMean", _statsHandler.getRequestTimeMean()); + requests.put("requestsTimeMax", _statsHandler.getRequestTimeMax()); + requests.put("requestsTimeStdDev", _statsHandler.getRequestTimeStdDev()); + + requests.put("dispatched", _statsHandler.getDispatched()); + requests.put("dispatchedActive", _statsHandler.getDispatchedActive()); + requests.put("dispatchedActiveMax", _statsHandler.getDispatchedActiveMax()); + requests.put("dispatchedTimeTotal", _statsHandler.getDispatchedTimeTotal()); + requests.put("dispatchedTimeMean", _statsHandler.getDispatchedTimeMean()); + requests.put("dispatchedTimeMax", _statsHandler.getDispatchedTimeMax()); + requests.put("dispatchedTimeStdDev", _statsHandler.getDispatchedTimeStdDev()); + + requests.put("asyncRequests", _statsHandler.getAsyncRequests()); + requests.put("requestsSuspended", _statsHandler.getAsyncDispatches()); + requests.put("requestsSuspendedMax", _statsHandler.getAsyncRequestsWaiting()); + requests.put("requestsResumed", _statsHandler.getAsyncRequestsWaitingMax()); + requests.put("requestsExpired", _statsHandler.getExpires()); + + requests.put("errors", _statsHandler.getErrors()); + + top.put("requests", requests); + + // responses + Map responses = new HashMap<>(); + responses.put("responses1xx", _statsHandler.getResponses1xx()); + responses.put("responses2xx", _statsHandler.getResponses2xx()); + responses.put("responses3xx", _statsHandler.getResponses3xx()); + responses.put("responses4xx", _statsHandler.getResponses4xx()); + responses.put("responses5xx", _statsHandler.getResponses5xx()); + responses.put("responsesBytesTotal", _statsHandler.getResponsesBytesTotal()); + top.put("responses", responses); + + // connections + List connections = new ArrayList<>(); + _connectors.forEach((connector) -> + { + Map connectorDetail = new HashMap<>(); + connectorDetail.put("name", String.format("%s@%X", connector.getClass().getName(), connector.hashCode())); + connectorDetail.put("protocols", connector.getProtocols()); ConnectionStatistics connectionStats = null; if (connector instanceof AbstractConnector) connectionStats = connector.getBean(ConnectionStatistics.class); if (connectionStats != null) { - sb.append(" true\n"); - sb.append(" ").append(connectionStats.getConnectionsTotal()).append("\n"); - sb.append(" ").append(connectionStats.getConnections()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionsMax()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationMean()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationMax()).append("\n"); - sb.append(" ").append(connectionStats.getConnectionDurationStdDev()).append("\n"); - sb.append(" ").append(connectionStats.getReceivedBytes()).append("\n"); - sb.append(" ").append(connectionStats.getSentBytes()).append("\n"); - sb.append(" ").append(connectionStats.getReceivedMessages()).append("\n"); - sb.append(" ").append(connectionStats.getSentMessages()).append("\n"); + connectorDetail.put("statsOn", true); + connectorDetail.put("connections", connectionStats.getConnectionsTotal()); + connectorDetail.put("connectionsOpen>", connectionStats.getConnections()); + connectorDetail.put("connectionsOpenMax", connectionStats.getConnectionsMax()); + connectorDetail.put("connectionsDurationMean", connectionStats.getConnectionDurationMean()); + connectorDetail.put("connectionsDurationMax", connectionStats.getConnectionDurationMax()); + connectorDetail.put("connectionsDurationStdDev", connectionStats.getConnectionDurationStdDev()); + connectorDetail.put("bytesIn", connectionStats.getReceivedBytes()); + connectorDetail.put("bytesOut", connectionStats.getSentBytes()); + connectorDetail.put("messagesIn", connectionStats.getReceivedMessages()); + connectorDetail.put("messagesOut", connectionStats.getSentMessages()); } else { + // Support for deprecated ConnectorStatistics (will be dropped in Jetty 10+) ConnectorStatistics connectorStats = null; if (connector instanceof AbstractConnector) connectorStats = connector.getBean(ConnectorStatistics.class); if (connectorStats != null) { - sb.append(" true\n"); - sb.append(" ").append(connectorStats.getConnections()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionsOpen()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionsOpenMax()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationMean()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationMax()).append("\n"); - sb.append(" ").append(connectorStats.getConnectionDurationStdDev()).append("\n"); - sb.append(" ").append(connectorStats.getMessagesIn()).append("\n"); - sb.append(" ").append(connectorStats.getMessagesIn()).append("\n"); - sb.append(" ").append(connectorStats.getStartedMillis()).append("\n"); + connectorDetail.put("statsOn", true); + connectorDetail.put("connections", connectorStats.getConnections()); + connectorDetail.put("connectionsOpen", connectorStats.getConnectionsOpen()); + connectorDetail.put("connectionsOpenMax", connectorStats.getConnectionsOpenMax()); + connectorDetail.put("connectionsDurationMean", connectorStats.getConnectionDurationMean()); + connectorDetail.put("connectionsDurationMax", connectorStats.getConnectionDurationMax()); + connectorDetail.put("connectionsDurationStdDev", connectorStats.getConnectionDurationStdDev()); + connectorDetail.put("messagesIn", connectorStats.getMessagesIn()); + connectorDetail.put("messagesOut", connectorStats.getMessagesIn()); + connectorDetail.put("elapsedMs", connectorStats.getStartedMillis()); } else { - sb.append(" false\n"); + connectorDetail.put("statsOn", false); } } - sb.append(" \n"); + connections.add(connectorDetail); + }); + top.put("connections", connections); + + // memory + Map memoryMap = new HashMap<>(); + memoryMap.put("heapMemoryUsage", _memoryBean.getHeapMemoryUsage().getUsed()); + memoryMap.put("nonHeapMemoryUsage", _memoryBean.getNonHeapMemoryUsage().getUsed()); + top.put("memory", memoryMap); + + // the top level object + return outputProducer.generate("statistics", top); + } + + private interface OutputProducer + { + CharSequence generate(String id, Map map); + } + + private static class JsonProducer implements OutputProducer + { + @Override + public CharSequence generate(String id, Map map) + { + return JSON.toString(map); + } + } + + private static class XmlProducer implements OutputProducer + { + private final StringBuilder sb; + private int indent = 0; + + public XmlProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + add(id, map); + return sb; + } + + private void indent() + { + sb.append("\n"); + for (int i = 0; i < indent; i++) + { + sb.append(' ').append(' '); + } } - sb.append(" \n"); - sb.append(" \n"); - sb.append(" ").append(_memoryBean.getHeapMemoryUsage().getUsed()).append("\n"); - sb.append(" ").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append("\n"); - sb.append(" \n"); + private void add(String id, Object obj) + { + sb.append('<').append(StringUtil.sanitizeXmlString(id)).append('>'); + indent++; - sb.append("\n"); + boolean wasIndented = false; - response.setContentType("text/xml"); - PrintWriter pout = response.getWriter(); - pout.write(sb.toString()); + if (obj instanceof Map) + { + //noinspection unchecked + addMap((Map)obj); + wasIndented = true; + } + else if (obj instanceof List) + { + addList(id, (List)obj); + wasIndented = true; + } + else + { + addObject(obj); + } + + indent--; + if (wasIndented) + indent(); + sb.append("'); + } + + private void addMap(Map map) + { + map.keySet().stream().sorted() + .forEach((key) -> + { + indent(); + add(key, map.get(key)); + }); + } + + private void addList(String parentId, List list) + { + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> + { + indent(); + add(childName, entry); + }); + } + + private void addObject(Object obj) + { + sb.append(StringUtil.sanitizeXmlString(Objects.toString(obj))); + } } - private void sendTextResponse(HttpServletResponse response) throws IOException + private static class TextProducer implements OutputProducer { - StringBuilder sb = new StringBuilder(); - sb.append(_statsHandler.toStatsHTML()); + private final StringBuilder sb; + private int indent = 0; + + public TextProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + add(id, map); + return sb; + } - sb.append("

Connections:

\n"); - for (Connector connector : _connectors) + private void indent() { - sb.append("

").append(connector.getClass().getName()).append("@").append(connector.hashCode()).append("

"); - sb.append("Protocols:"); - for (String protocol : connector.getProtocols()) + for (int i = 0; i < indent; i++) { - sb.append(protocol).append(" "); + sb.append(' ').append(' '); } - sb.append("
\n"); + } - ConnectionStatistics connectionStats = null; - if (connector instanceof Container) - connectionStats = connector.getBean(ConnectionStatistics.class); - if (connectionStats != null) + private void add(String id, Object obj) + { + indent(); + sb.append(id).append(": "); + indent++; + + if (obj instanceof Map) + { + sb.append('\n'); + //noinspection unchecked + addMap((Map)obj); + } + else if (obj instanceof List) { - sb.append("Total connections: ").append(connectionStats.getConnectionsTotal()).append("
\n"); - sb.append("Current connections open: ").append(connectionStats.getConnections()).append("
\n"); - sb.append("Max concurrent connections open: ").append(connectionStats.getConnectionsMax()).append("
\n"); - sb.append("Mean connection duration: ").append(connectionStats.getConnectionDurationMean()).append("
\n"); - sb.append("Max connection duration: ").append(connectionStats.getConnectionDurationMax()).append("
\n"); - sb.append("Connection duration standard deviation: ").append(connectionStats.getConnectionDurationStdDev()).append("
\n"); - sb.append("Total bytes received: ").append(connectionStats.getReceivedBytes()).append("
\n"); - sb.append("Total bytes sent: ").append(connectionStats.getSentBytes()).append("
\n"); - sb.append("Total messages received: ").append(connectionStats.getReceivedMessages()).append("
\n"); - sb.append("Total messages sent: ").append(connectionStats.getSentMessages()).append("
\n"); + sb.append('\n'); + addList(id, (List)obj); } else { - ConnectorStatistics connectorStats = null; - if (connector instanceof AbstractConnector) - connectorStats = connector.getBean(ConnectorStatistics.class); - if (connectorStats != null) - { - sb.append("Statistics gathering started ").append(connectorStats.getStartedMillis()).append("ms ago").append("
\n"); - sb.append("Total connections: ").append(connectorStats.getConnections()).append("
\n"); - sb.append("Current connections open: ").append(connectorStats.getConnectionsOpen()).append("
\n"); - sb.append("Max concurrent connections open: ").append(connectorStats.getConnectionsOpenMax()).append("
\n"); - sb.append("Mean connection duration: ").append(connectorStats.getConnectionDurationMean()).append("
\n"); - sb.append("Max connection duration: ").append(connectorStats.getConnectionDurationMax()).append("
\n"); - sb.append("Connection duration standard deviation: ").append(connectorStats.getConnectionDurationStdDev()).append("
\n"); - sb.append("Total messages in: ").append(connectorStats.getMessagesIn()).append("
\n"); - sb.append("Total messages out: ").append(connectorStats.getMessagesOut()).append("
\n"); - } - else - { - sb.append("Statistics gathering off.\n"); - } + addObject(obj); + sb.append('\n'); } + + indent--; } - sb.append("

Memory:

\n"); - sb.append("Heap memory usage: ").append(_memoryBean.getHeapMemoryUsage().getUsed()).append(" bytes").append("
\n"); - sb.append("Non-heap memory usage: ").append(_memoryBean.getNonHeapMemoryUsage().getUsed()).append(" bytes").append("
\n"); + private void addMap(Map map) + { + map.keySet().stream().sorted() + .forEach((key) -> add(key, map.get(key))); + } - response.setContentType("text/html"); - PrintWriter pout = response.getWriter(); - pout.write(sb.toString()); + private void addList(String parentId, List list) + { + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> add(childName, entry)); + } + + private void addObject(Object obj) + { + sb.append(obj); + } + } + + private static class HtmlProducer implements OutputProducer + { + private final StringBuilder sb; + private int indent = 0; + + public HtmlProducer() + { + this.sb = new StringBuilder(); + } + + @Override + public CharSequence generate(String id, Map map) + { + sb.append("
    \n"); + add(id, map); + sb.append("
\n"); + return sb; + } + + private void indent() + { + for (int i = 0; i < indent; i++) + { + sb.append(' ').append(' '); + } + } + + private void add(String id, Object obj) + { + indent(); + indent++; + sb.append("
  • ").append(StringUtil.sanitizeXmlString(id)).append(": "); + if (obj instanceof Map) + { + //noinspection unchecked + addMap((Map)obj); + indent(); + } + else if (obj instanceof List) + { + addList(id, (List)obj); + indent(); + } + else + { + addObject(obj); + } + sb.append("
  • \n"); + + indent--; + } + + private void addMap(Map map) + { + sb.append("\n"); + indent(); + sb.append("
      \n"); + indent++; + map.keySet().stream().sorted(String::compareToIgnoreCase) + .forEach((key) -> add(key, map.get(key))); + indent--; + indent(); + sb.append("
    \n"); + } + + private void addList(String parentId, List list) + { + sb.append("\n"); + indent(); + sb.append("
      \n"); + indent++; + // drop the 's' at the end. + String childName = parentId.replaceFirst("s$", ""); + list.forEach((entry) -> add(childName, entry)); + indent--; + indent(); + sb.append("
    \n"); + } + + private void addObject(Object obj) + { + sb.append(StringUtil.sanitizeXmlString(Objects.toString(obj))); + } } } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java index ef99d909e3bf..5590d9bd2850 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java @@ -18,31 +18,42 @@ package org.eclipse.jetty.servlet; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; import java.nio.ByteBuffer; +import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpTester; import org.eclipse.jetty.http.HttpVersion; import org.eclipse.jetty.server.LocalConnector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.ajax.JSON; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; import org.xml.sax.InputSource; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; public class StatisticsServletTest { @@ -66,9 +77,7 @@ public void destroyServer() _server.join(); } - @Test - public void getStats() - throws Exception + private void addStatisticsHandler() { StatisticsHandler statsHandler = new StatisticsHandler(); _server.setHandler(statsHandler); @@ -78,30 +87,196 @@ public void getStats() servletHolder.setInitParameter("restrictToLocalhost", "false"); statsContext.addServlet(servletHolder, "/stats"); statsContext.setSessionHandler(new SessionHandler()); + } + + @Test + public void getStats() + throws Exception + { + addStatisticsHandler(); _server.start(); - getResponse("/test1"); - String response = getResponse("/stats?xml=true"); - Stats stats = parseStats(response); + HttpTester.Response response; + + // Trigger 2xx response + response = getResponse("/test1"); + assertEquals(response.getStatus(), 200); + + // Look for 200 response that was tracked + response = getResponse("/stats?xml=true"); + assertEquals(response.getStatus(), 200); + Stats stats = parseStats(response.getContent()); assertEquals(1, stats.responses2xx); - getResponse("/stats?statsReset=true"); + // Reset stats + response = getResponse("/stats?statsReset=true"); + assertEquals(response.getStatus(), 200); + + // Request stats again response = getResponse("/stats?xml=true"); - stats = parseStats(response); + assertEquals(response.getStatus(), 200); + stats = parseStats(response.getContent()); assertEquals(1, stats.responses2xx); - getResponse("/test1"); - getResponse("/nothing"); + // Trigger 2xx response + response = getResponse("/test1"); + assertEquals(response.getStatus(), 200); + // Trigger 4xx response + response = getResponse("/nothing"); + assertEquals(response.getStatus(), 404); + + // Request stats again response = getResponse("/stats?xml=true"); - stats = parseStats(response); + assertEquals(response.getStatus(), 200); + stats = parseStats(response.getContent()); + // Verify we see (from last reset) + // 1) request for /stats?statsReset=true [2xx] + // 2) request for /stats?xml=true [2xx] + // 3) request for /test1 [2xx] + // 4) request for /nothing [4xx] assertThat("2XX Response Count" + response, stats.responses2xx, is(3)); assertThat("4XX Response Count" + response, stats.responses4xx, is(1)); } - public String getResponse(String path) + @Test + public void getXmlResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/xml"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/xml")); + + // System.out.println(response.getContent()); + + // Parse it, make sure it's well formed. + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + try (ByteArrayInputStream input = new ByteArrayInputStream(response.getContentBytes())) + { + Document doc = docBuilder.parse(input); + assertNotNull(doc); + assertEquals("statistics", doc.getDocumentElement().getNodeName()); + } + } + + @Test + public void getJsonResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "application/json"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), is("application/json")); + assertThat("Response.contentType for json should never contain a charset", + response.get(HttpHeader.CONTENT_TYPE), not(containsString("charset"))); + + // System.out.println(response.getContent()); + + // Parse it, make sure it's well formed. + Object doc = JSON.parse(response.getContent()); + assertNotNull(doc); + assertThat(doc, instanceOf(Map.class)); + Map docMap = (Map)doc; + assertEquals(4, docMap.size()); + assertNotNull(docMap.get("requests")); + assertNotNull(docMap.get("responses")); + assertNotNull(docMap.get("connections")); + assertNotNull(docMap.get("memory")); + } + + @Test + public void getTextResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/plain"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/plain")); + + // System.out.println(response.getContent()); + + // Look for expected content + assertThat(response.getContent(), containsString("requests: ")); + assertThat(response.getContent(), containsString("responses: ")); + assertThat(response.getContent(), containsString("connections: ")); + assertThat(response.getContent(), containsString("memory: ")); + } + + @Test + public void getHtmlResponse() + throws Exception + { + addStatisticsHandler(); + _server.start(); + + HttpTester.Response response; + HttpTester.Request request = new HttpTester.Request(); + + request.setMethod("GET"); + request.setURI("/stats"); + request.setHeader("Accept", "text/html"); + request.setVersion(HttpVersion.HTTP_1_1); + request.setHeader("Host", "test"); + + ByteBuffer responseBuffer = _connector.getResponse(request.generate()); + response = HttpTester.parseResponse(responseBuffer); + + assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html")); + + System.out.println(response.getContent()); + + // Look for things that indicate it's a well formed HTML output + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("requests: ")); + assertThat(response.getContent(), containsString("responses: ")); + assertThat(response.getContent(), containsString("connections: ")); + assertThat(response.getContent(), containsString("memory: ")); + assertThat(response.getContent(), containsString("")); + assertThat(response.getContent(), containsString("")); + } + + public HttpTester.Response getResponse(String path) throws Exception { HttpTester.Request request = new HttpTester.Request(); @@ -111,7 +286,7 @@ public String getResponse(String path) request.setHeader("Host", "test"); ByteBuffer responseBuffer = _connector.getResponse(request.generate()); - return HttpTester.parseResponse(responseBuffer).getContent(); + return HttpTester.parseResponse(responseBuffer); } public Stats parseStats(String xml) @@ -120,7 +295,6 @@ public Stats parseStats(String xml) XPath xPath = XPathFactory.newInstance().newXPath(); String responses4xx = xPath.evaluate("//responses4xx", new InputSource(new StringReader(xml))); - String responses2xx = xPath.evaluate("//responses2xx", new InputSource(new StringReader(xml))); return new Stats(Integer.parseInt(responses2xx), Integer.parseInt(responses4xx)); From 02f35deeaac1ed056e5c69aedbed73eef582e0d0 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 11:07:21 -0600 Subject: [PATCH 02/10] Issue #5539 - Deprecating 'xml' request parameter in StatisticsServlet Signed-off-by: Joakim Erdfelt --- .../jetty/servlet/StatisticsServlet.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index 3c6f325dd8b4..889f7f3b2f25 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -118,6 +118,11 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t return; } + if (request.getParameter("xml") != null) + { + LOG.warn("'xml' parameter is deprecated, use 'Accept' request header instead"); + } + List acceptable = getOrderedAcceptableMimeTypes(request); for (String mimeType : acceptable) @@ -134,21 +139,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) t writeHtmlResponse(response); return; case "text/plain": - writeTextResponse(response); - return; case "*/*": - String wantXml = request.getParameter("xml"); - if (wantXml == null) - wantXml = request.getParameter("XML"); - - if (Boolean.parseBoolean(wantXml)) - { - writeXmlResponse(response); - } - else - { - writeTextResponse(response); - } + writeTextResponse(response); return; default: if (LOG.isDebugEnabled()) From b419db90c5c2186f4f244db47c74cf330d1e1367 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 11:15:43 -0600 Subject: [PATCH 03/10] Issue #5539 - Cleanup, adding javadoc, etc. Signed-off-by: Joakim Erdfelt --- .../jetty/servlet/StatisticsServlet.java | 13 ++++++++++++ .../jetty/servlet/StatisticsServletTest.java | 20 ++++++++++--------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index 889f7f3b2f25..a453e1326386 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -54,6 +54,19 @@ import static java.nio.charset.StandardCharsets.UTF_8; +/** + * Collect and report statistics about requests / responses / connections and more. + *

    + * You can use normal HTTP content negotiation to ask for the statistics. + * Specify a request Accept header for one of the following formats: + *

      + *
    • application/json
    • + *
    • text/xml
    • + *
    • text/html
    • + *
    • text/plain - default if no Accept header specified
    • + *
    + *

    + */ public class StatisticsServlet extends HttpServlet { private static final Logger LOG = Log.getLogger(StatisticsServlet.class); diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java index 5590d9bd2850..576b961f631a 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java @@ -90,7 +90,7 @@ private void addStatisticsHandler() } @Test - public void getStats() + public void testGetStats() throws Exception { addStatisticsHandler(); @@ -103,7 +103,7 @@ public void getStats() assertEquals(response.getStatus(), 200); // Look for 200 response that was tracked - response = getResponse("/stats?xml=true"); + response = getResponse("/stats"); assertEquals(response.getStatus(), 200); Stats stats = parseStats(response.getContent()); @@ -114,7 +114,7 @@ public void getStats() assertEquals(response.getStatus(), 200); // Request stats again - response = getResponse("/stats?xml=true"); + response = getResponse("/stats"); assertEquals(response.getStatus(), 200); stats = parseStats(response.getContent()); @@ -128,7 +128,7 @@ public void getStats() assertEquals(response.getStatus(), 404); // Request stats again - response = getResponse("/stats?xml=true"); + response = getResponse("/stats"); assertEquals(response.getStatus(), 200); stats = parseStats(response.getContent()); @@ -142,7 +142,7 @@ public void getStats() } @Test - public void getXmlResponse() + public void testGetXmlResponse() throws Exception { addStatisticsHandler(); @@ -166,6 +166,7 @@ public void getXmlResponse() // Parse it, make sure it's well formed. DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + docBuilderFactory.setValidating(false); DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); try (ByteArrayInputStream input = new ByteArrayInputStream(response.getContentBytes())) { @@ -176,7 +177,7 @@ public void getXmlResponse() } @Test - public void getJsonResponse() + public void testGetJsonResponse() throws Exception { addStatisticsHandler(); @@ -213,7 +214,7 @@ public void getJsonResponse() } @Test - public void getTextResponse() + public void testGetTextResponse() throws Exception { addStatisticsHandler(); @@ -243,7 +244,7 @@ public void getTextResponse() } @Test - public void getHtmlResponse() + public void testGetHtmlResponse() throws Exception { addStatisticsHandler(); @@ -263,7 +264,7 @@ public void getHtmlResponse() assertThat("Response.contentType", response.get(HttpHeader.CONTENT_TYPE), containsString("text/html")); - System.out.println(response.getContent()); + // System.out.println(response.getContent()); // Look for things that indicate it's a well formed HTML output assertThat(response.getContent(), containsString("")); @@ -281,6 +282,7 @@ public HttpTester.Response getResponse(String path) { HttpTester.Request request = new HttpTester.Request(); request.setMethod("GET"); + request.setHeader("Accept", "text/xml"); request.setURI(path); request.setVersion(HttpVersion.HTTP_1_1); request.setHeader("Host", "test"); From cc952f3a3ac4db08e2e5996ea7b08ed01a854133 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 11:38:26 -0600 Subject: [PATCH 04/10] Issue #5539 - Adding query parameter accept variation Signed-off-by: Joakim Erdfelt --- .../jetty/servlet/StatisticsServlet.java | 11 ++- .../jetty/servlet/StatisticsServletTest.java | 96 +++++++++++++++---- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index a453e1326386..affba5955603 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -235,7 +235,16 @@ private List getOrderedAcceptableMimeTypes(HttpServletRequest request) return values.getValues(); } - // No accept specified, return that we allow ALL mime types + // No accept header specified, try 'accept' parameter (for those clients that are + // so ancient that they cannot set the standard HTTP `Accept` header) + String acceptParameter = request.getParameter("accept"); + if (acceptParameter != null) + { + // return that we support the one type specified in the 'accept' parameter + return Collections.singletonList(acceptParameter); + } + + // return that we allow ALL mime types return Collections.singletonList("*/*"); } diff --git a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java index 576b961f631a..8b5d406094b0 100644 --- a/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java +++ b/jetty-servlet/src/test/java/org/eclipse/jetty/servlet/StatisticsServletTest.java @@ -24,6 +24,8 @@ import java.io.StringReader; import java.nio.ByteBuffer; import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -44,6 +46,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; 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 org.w3c.dom.Document; import org.xml.sax.InputSource; @@ -141,8 +146,53 @@ public void testGetStats() assertThat("4XX Response Count" + response, stats.responses4xx, is(1)); } - @Test - public void testGetXmlResponse() + public static Stream typeVariations(String mimeType) + { + return Stream.of( + Arguments.of( + new Consumer() + { + @Override + public void accept(HttpTester.Request request) + { + request.setURI("/stats"); + request.setHeader("Accept", mimeType); + } + + @Override + public String toString() + { + return "Header[Accept: " + mimeType + "]"; + } + } + ), + Arguments.of( + new Consumer() + { + @Override + public void accept(HttpTester.Request request) + { + request.setURI("/stats?accept=" + mimeType); + } + + @Override + public String toString() + { + return "query[accept=" + mimeType + "]"; + } + } + ) + ); + } + + public static Stream xmlVariations() + { + return typeVariations("text/xml"); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("xmlVariations") + public void testGetXmlResponse(Consumer requestCustomizer) throws Exception { addStatisticsHandler(); @@ -152,10 +202,9 @@ public void testGetXmlResponse() HttpTester.Request request = new HttpTester.Request(); request.setMethod("GET"); - request.setURI("/stats"); - request.setHeader("Accept", "text/xml"); request.setVersion(HttpVersion.HTTP_1_1); request.setHeader("Host", "test"); + requestCustomizer.accept(request); ByteBuffer responseBuffer = _connector.getResponse(request.generate()); response = HttpTester.parseResponse(responseBuffer); @@ -176,8 +225,14 @@ public void testGetXmlResponse() } } - @Test - public void testGetJsonResponse() + public static Stream jsonVariations() + { + return typeVariations("application/json"); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("jsonVariations") + public void testGetJsonResponse(Consumer requestCustomizer) throws Exception { addStatisticsHandler(); @@ -187,8 +242,7 @@ public void testGetJsonResponse() HttpTester.Request request = new HttpTester.Request(); request.setMethod("GET"); - request.setURI("/stats"); - request.setHeader("Accept", "application/json"); + requestCustomizer.accept(request); request.setVersion(HttpVersion.HTTP_1_1); request.setHeader("Host", "test"); @@ -213,8 +267,14 @@ public void testGetJsonResponse() assertNotNull(docMap.get("memory")); } - @Test - public void testGetTextResponse() + public static Stream plaintextVariations() + { + return typeVariations("text/plain"); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("plaintextVariations") + public void testGetTextResponse(Consumer requestCustomizer) throws Exception { addStatisticsHandler(); @@ -224,8 +284,7 @@ public void testGetTextResponse() HttpTester.Request request = new HttpTester.Request(); request.setMethod("GET"); - request.setURI("/stats"); - request.setHeader("Accept", "text/plain"); + requestCustomizer.accept(request); request.setVersion(HttpVersion.HTTP_1_1); request.setHeader("Host", "test"); @@ -243,8 +302,14 @@ public void testGetTextResponse() assertThat(response.getContent(), containsString("memory: ")); } - @Test - public void testGetHtmlResponse() + public static Stream htmlVariations() + { + return typeVariations("text/html"); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("htmlVariations") + public void testGetHtmlResponse(Consumer requestCustomizer) throws Exception { addStatisticsHandler(); @@ -254,8 +319,7 @@ public void testGetHtmlResponse() HttpTester.Request request = new HttpTester.Request(); request.setMethod("GET"); - request.setURI("/stats"); - request.setHeader("Accept", "text/html"); + requestCustomizer.accept(request); request.setVersion(HttpVersion.HTTP_1_1); request.setHeader("Host", "test"); From c51127558038f1c6b39e4a7f75bfdec9ac3d9fba Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 11:47:49 -0600 Subject: [PATCH 05/10] Provide more detail in test failure on CI Signed-off-by: Joakim Erdfelt --- .../org/eclipse/jetty/webapp/WebInfConfigurationTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java index d5a3c20a738e..962c0eb8e054 100644 --- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.nio.file.Path; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jetty.toolchain.test.FS; @@ -72,7 +73,7 @@ public void testFindAndFilterContainerPaths() context.setClassLoader(loader); config.findAndFilterContainerPaths(context); List containerResources = context.getMetaData().getContainerResources(); - assertEquals(1, containerResources.size()); + assertEquals(1, containerResources.size(), () -> containerResources.stream().map(Resource::toString).collect(Collectors.joining(",", "[", "]"))); assertThat(containerResources.get(0).toString(), containsString("jetty-util")); } @@ -93,7 +94,7 @@ public void testFindAndFilterContainerPathsJDK9() context.setClassLoader(loader); config.findAndFilterContainerPaths(context); List containerResources = context.getMetaData().getContainerResources(); - assertEquals(2, containerResources.size()); + assertEquals(2, containerResources.size(), () -> containerResources.stream().map(Resource::toString).collect(Collectors.joining(",", "[", "]"))); for (Resource r : containerResources) { String s = r.toString(); @@ -121,7 +122,7 @@ public void testFindAndFilterContainerPathsTarget8() context.setClassLoader(loader); config.findAndFilterContainerPaths(context); List containerResources = context.getMetaData().getContainerResources(); - assertEquals(1, containerResources.size()); + assertEquals(1, containerResources.size(), () -> containerResources.stream().map(Resource::toString).collect(Collectors.joining(",", "[", "]"))); assertThat(containerResources.get(0).toString(), containsString("jetty-util")); } From c9440357a732fd465fa8a765827a312e644330e0 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 11:57:26 -0600 Subject: [PATCH 06/10] Issue #5539 - Fixing javadoc Signed-off-by: Joakim Erdfelt --- .../org/eclipse/jetty/servlet/StatisticsServlet.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java index affba5955603..0e0acb407176 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/StatisticsServlet.java @@ -59,13 +59,13 @@ *

    * You can use normal HTTP content negotiation to ask for the statistics. * Specify a request Accept header for one of the following formats: - *

      - *
    • application/json
    • - *
    • text/xml
    • - *
    • text/html
    • - *
    • text/plain - default if no Accept header specified
    • - *
    *

    + *
      + *
    • application/json
    • + *
    • text/xml
    • + *
    • text/html
    • + *
    • text/plain - default if no Accept header specified
    • + *
    */ public class StatisticsServlet extends HttpServlet { From fd974c474c1175b420e0d187771b3f2eefb15ffe Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 12:33:12 -0600 Subject: [PATCH 07/10] Fixing bad Container Include Jar pattern. + It was matching on jetty-util-ajax-#.jar as well. Signed-off-by: Joakim Erdfelt --- .../org/eclipse/jetty/webapp/WebInfConfigurationTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java index 962c0eb8e054..aa8718c53699 100644 --- a/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java +++ b/jetty-webapp/src/test/java/org/eclipse/jetty/webapp/WebInfConfigurationTest.java @@ -67,7 +67,7 @@ public void testFindAndFilterContainerPaths() { WebInfConfiguration config = new WebInfConfiguration(); WebAppContext context = new WebAppContext(); - context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/"); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[0-9][^/]*\\.jar$|.*/jetty-util/target/classes/"); WebAppClassLoader loader = new WebAppClassLoader(context); context.setClassLoader(loader); @@ -89,7 +89,7 @@ public void testFindAndFilterContainerPathsJDK9() { WebInfConfiguration config = new WebInfConfiguration(); WebAppContext context = new WebAppContext(); - context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[0-9][^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); WebAppClassLoader loader = new WebAppClassLoader(context); context.setClassLoader(loader); config.findAndFilterContainerPaths(context); @@ -117,7 +117,7 @@ public void testFindAndFilterContainerPathsTarget8() WebInfConfiguration config = new WebInfConfiguration(); WebAppContext context = new WebAppContext(); context.setAttribute(JavaVersion.JAVA_TARGET_PLATFORM, "8"); - context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); + context.setAttribute(WebInfConfiguration.CONTAINER_JAR_PATTERN, ".*/jetty-util-[0-9][^/]*\\.jar$|.*/jetty-util/target/classes/$|.*/foo-bar-janb.jar"); WebAppClassLoader loader = new WebAppClassLoader(context); context.setClassLoader(loader); config.findAndFilterContainerPaths(context); From 770be2dbc3a2fe4430310a2404baed8653a92519 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Mon, 2 Nov 2020 14:51:44 -0600 Subject: [PATCH 08/10] Issue #5539 - Adding jetty-util-ajax missing dep to osgi tests Signed-off-by: Joakim Erdfelt --- .../src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java index ebfdf527f7e0..94e2257acb9e 100644 --- a/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java +++ b/jetty-osgi/test-jetty-osgi/src/test/java/org/eclipse/jetty/osgi/test/TestOSGiUtil.java @@ -133,6 +133,7 @@ public static List