From fb6a445639aaf0f6068db044a959dd277c4b5ab7 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Thu, 28 May 2020 12:17:22 +0200 Subject: [PATCH] Issue #4741 No Servlet Path (#4898) * Issue #4741 HttpServletMapping This completes the refactoring started in #4851, using the HttpServletMapping field to avoid having the servletPath field in the Request and instead have a pathInContext field. Signed-off-by: Greg Wilkins * Issue #4741 HttpServletMapping reverted ResourceService changes Signed-off-by: Greg Wilkins * Issue #4741 HttpServletMapping fixed gzip handler Signed-off-by: Greg Wilkins * Issue #4741 HttpServletMapping Fixed several TODOs left in the code removed _contextPath field and used an attributes lookup for include replaced setContextPaths with setContext Signed-off-by: Greg Wilkins * Issue #4741 HttpServletMapping Used the same pattern from the contextPath changes for servletPath and pathInfo. Now the servletPathMapping is always set on the request and only if the dispatch is an include do the effected methods look deeper for the source values. Signed-off-by: Greg Wilkins * Issue #4741 HttpServletMapping Improved javadoc Signed-off-by: Greg Wilkins --- .../security/openid/OpenIdAuthenticator.java | 2 +- .../jetty/rewrite/handler/RuleContainer.java | 2 +- .../rewrite/handler/RewriteHandlerTest.java | 16 +- .../authentication/FormAuthenticator.java | 2 +- .../org/eclipse/jetty/server/Dispatcher.java | 107 ++++++----- .../org/eclipse/jetty/server/Request.java | 175 +++++++++++------- .../org/eclipse/jetty/server/Response.java | 2 +- .../java/org/eclipse/jetty/server/Server.java | 2 +- .../handler/BufferedResponseHandler.java | 3 +- .../jetty/server/handler/ContextHandler.java | 51 ++--- .../server/handler/gzip/GzipHandler.java | 3 +- .../org/eclipse/jetty/server/RequestTest.java | 26 --- .../eclipse/jetty/server/ResponseTest.java | 54 ++++-- .../eclipse/jetty/servlet/ServletHandler.java | 9 +- .../org/eclipse/jetty/util/Attributes.java | 24 +++ 15 files changed, 262 insertions(+), 216 deletions(-) diff --git a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java index 32a5d7a4270..8b1b829d7ce 100644 --- a/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java +++ b/jetty-openid/src/main/java/org/eclipse/jetty/security/openid/OpenIdAuthenticator.java @@ -249,7 +249,7 @@ public class OpenIdAuthenticator extends LoginAuthenticator if (!mandatory) return new DeferredAuthentication(this); - if (isErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) + if (isErrorPage(baseRequest.getPathInContext()) && !DeferredAuthentication.isDeferred(response)) return new DeferredAuthentication(this); try diff --git a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java index 860eb0a05cf..8f043ebe3ac 100644 --- a/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java +++ b/jetty-rewrite/src/main/java/org/eclipse/jetty/rewrite/handler/RuleContainer.java @@ -195,7 +195,7 @@ public class RuleContainer extends Rule implements Dumpable } if (_rewritePathInfo) - baseRequest.setPathInfo(applied); + baseRequest.setContext(baseRequest.getContext(), applied); target = applied; diff --git a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java index ab5da55f4bc..91a47bd537b 100644 --- a/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java +++ b/jetty-rewrite/src/test/java/org/eclipse/jetty/rewrite/handler/RewriteHandlerTest.java @@ -85,7 +85,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _handler.setRewriteRequestURI(true); _handler.setRewritePathInfo(true); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/bar")); - _request.setPathInfo("/xxx/bar"); + _request.setContext(_request.getContext(), "/xxx/bar"); _handler.handle("/xxx/bar", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/bar/zzz", _request.getAttribute("target")); @@ -99,7 +99,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _handler.setRewriteRequestURI(false); _handler.setRewritePathInfo(false); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/foo/bar")); - _request.setPathInfo("/foo/bar"); + _request.setContext(_request.getContext(), "/foo/bar"); _handler.handle("/foo/bar", _request, _request, _response); assertEquals(201, _response.getStatus()); @@ -112,7 +112,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _request.setHandled(false); _handler.setOriginalPathAttribute(null); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); - _request.setPathInfo("/aaa/bar"); + _request.setContext(_request.getContext(), "/aaa/bar"); _handler.handle("/aaa/bar", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/ddd/bar", _request.getAttribute("target")); @@ -126,7 +126,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _handler.setRewriteRequestURI(true); _handler.setRewritePathInfo(true); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); - _request.setPathInfo("/aaa/bar"); + _request.setContext(_request.getContext(), "/aaa/bar"); _handler.handle("/aaa/bar", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/ddd/bar", _request.getAttribute("target")); @@ -138,7 +138,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _request.setHandled(false); _rule2.setTerminating(true); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); - _request.setPathInfo("/aaa/bar"); + _request.setContext(_request.getContext(), "/aaa/bar"); _handler.handle("/aaa/bar", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/ccc/bar", _request.getAttribute("target")); @@ -154,7 +154,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _request.setAttribute("URI", null); _request.setAttribute("info", null); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/aaa/bar")); - _request.setPathInfo("/aaa/bar"); + _request.setContext(_request.getContext(), "/aaa/bar"); _handler.handle("/aaa/bar", _request, _request, _response); assertEquals(200, _response.getStatus()); assertEquals(null, _request.getAttribute("target")); @@ -173,7 +173,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _handler.setRewriteRequestURI(true); _handler.setRewritePathInfo(false); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/ccc/x%20y")); - _request.setPathInfo("/ccc/x y"); + _request.setContext(_request.getContext(), "/ccc/x y"); _handler.handle("/ccc/x y", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/ddd/x y", _request.getAttribute("target")); @@ -190,7 +190,7 @@ public class RewriteHandlerTest extends AbstractRuleTestCase _handler.setRewriteRequestURI(true); _handler.setRewritePathInfo(false); _request.setHttpURI(HttpURI.build(_request.getHttpURI(), "/xxx/x%20y")); - _request.setPathInfo("/xxx/x y"); + _request.setContext(_request.getContext(), "/xxx/x y"); _handler.handle("/xxx/x y", _request, _request, _response); assertEquals(201, _response.getStatus()); assertEquals("/x y/zzz", _request.getAttribute("target")); diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java index 383dfd6f274..919fa036d54 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/FormAuthenticator.java @@ -254,7 +254,7 @@ public class FormAuthenticator extends LoginAuthenticator if (!mandatory) return new DeferredAuthentication(this); - if (isLoginOrErrorPage(URIUtil.addPaths(request.getServletPath(), request.getPathInfo())) && !DeferredAuthentication.isDeferred(response)) + if (isLoginOrErrorPage(baseRequest.getPathInContext()) && !DeferredAuthentication.isDeferred(response)) return new DeferredAuthentication(this); try diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java index 086ba3cb16a..639a827b8d3 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Dispatcher.java @@ -90,6 +90,8 @@ public class Dispatcher implements RequestDispatcher final DispatcherType old_type = baseRequest.getDispatcherType(); final Attributes old_attr = baseRequest.getAttributes(); final MultiMap old_query_params = baseRequest.getQueryParameters(); + final ContextHandler.Context old_context = baseRequest.getContext(); + final ServletPathMapping old_mapping = baseRequest.getServletPathMapping(); try { baseRequest.setDispatcherType(DispatcherType.INCLUDE); @@ -100,7 +102,14 @@ public class Dispatcher implements RequestDispatcher } else { - IncludeAttributes attr = new IncludeAttributes(old_attr, _uri.getPath(), _contextHandler.getContextPath(), _pathInContext, _uri.getQuery()); + IncludeAttributes attr = new IncludeAttributes( + old_attr, + baseRequest, + old_context, + old_mapping, + _uri.getPath(), + _pathInContext, + _uri.getQuery()); if (attr._query != null) baseRequest.mergeQueryParameters(baseRequest.getQueryString(), attr._query); baseRequest.setAttributes(attr); @@ -136,11 +145,10 @@ public class Dispatcher implements RequestDispatcher response = new ServletResponseHttpWrapper(response); final HttpURI old_uri = baseRequest.getHttpURI(); - final String old_context_path = baseRequest.getContextPath(); - final String old_servlet_path = baseRequest.getServletPath(); - final String old_path_info = baseRequest.getPathInfo(); + final ContextHandler.Context old_context = baseRequest.getContext(); + final String old_path_in_context = baseRequest.getPathInContext(); final ServletPathMapping old_mapping = baseRequest.getServletPathMapping(); - + final ServletPathMapping source_mapping = baseRequest.findServletPathMapping(); final MultiMap old_query_params = baseRequest.getQueryParameters(); final Attributes old_attr = baseRequest.getAttributes(); final DispatcherType old_type = baseRequest.getDispatcherType(); @@ -161,30 +169,21 @@ public class Dispatcher implements RequestDispatcher // for queryString is allowed to be null, but cannot be null for the other values. // Note: the pathInfo is passed as the pathInContext since it is only used when there is // no mapping, and when there is no mapping the pathInfo is the pathInContext. - // TODO Ultimately it is intended for the request to carry the pathInContext for easy access - ForwardAttributes attr = old_attr.getAttribute(FORWARD_REQUEST_URI) != null - ? new ForwardAttributes(old_attr, - (String)old_attr.getAttribute(FORWARD_REQUEST_URI), - (String)old_attr.getAttribute(FORWARD_CONTEXT_PATH), - (String)old_attr.getAttribute(FORWARD_PATH_INFO), - (ServletPathMapping)old_attr.getAttribute(FORWARD_MAPPING), - (String)old_attr.getAttribute(FORWARD_QUERY_STRING)) - : new ForwardAttributes(old_attr, - old_uri.getPath(), - old_context_path, - baseRequest.getPathInfo(), // TODO replace with pathInContext - old_mapping, - old_uri.getQuery()); + if (old_attr.getAttribute(FORWARD_REQUEST_URI) == null) + baseRequest.setAttributes(new ForwardAttributes(old_attr, + old_uri.getPath(), + old_context == null ? null : old_context.getContextHandler().getContextPathEncoded(), + baseRequest.getPathInContext(), + source_mapping, + old_uri.getQuery())); String query = _uri.getQuery(); if (query == null) query = old_uri.getQuery(); baseRequest.setHttpURI(HttpURI.build(old_uri, _uri.getPath(), _uri.getParam(), query)); - baseRequest.setContextPath(_contextHandler.getContextPath()); + baseRequest.setContext(_contextHandler.getServletContext(), _pathInContext); baseRequest.setServletPathMapping(null); - baseRequest.setServletPath(null); - baseRequest.setPathInfo(_pathInContext); if (_uri.getQuery() != null || old_uri.getQuery() != null) { @@ -207,8 +206,6 @@ public class Dispatcher implements RequestDispatcher } } - baseRequest.setAttributes(attr); - _contextHandler.handle(_pathInContext, baseRequest, (HttpServletRequest)request, (HttpServletResponse)response); // If we are not async and not closed already, then close via the possibly wrapped response. @@ -228,9 +225,8 @@ public class Dispatcher implements RequestDispatcher finally { baseRequest.setHttpURI(old_uri); - baseRequest.setContextPath(old_context_path); - baseRequest.setServletPath(old_servlet_path); - baseRequest.setPathInfo(old_path_info); + baseRequest.setContext(old_context, old_path_in_context); + baseRequest.setServletPathMapping(old_mapping); baseRequest.setQueryParameters(old_query_params); baseRequest.resetParameters(); baseRequest.setAttributes(old_attr); @@ -346,23 +342,44 @@ public class Dispatcher implements RequestDispatcher } } - private class IncludeAttributes extends Attributes.Wrapper + /** + * Attributes Wrapper to provide the {@link DispatcherType#INCLUDE} attributes. + * + * The source {@link org.eclipse.jetty.server.handler.ContextHandler.Context} and + * {@link ServletPathMapping} instances are also retained by this wrapper so they + * may be used by {@link Request#getContextPath()}, {@link Request#getServletPath()}, + * {@link Request#getPathInfo()} and {@link Request#getHttpServletMapping()}. + */ + class IncludeAttributes extends Attributes.Wrapper { + private final Request _baseRequest; + private final ContextHandler.Context _sourceContext; + private final ServletPathMapping _sourceMapping; private final String _requestURI; - private final String _contextPath; private final String _pathInContext; - private ServletPathMapping _servletPathMapping; // Set later by ServletHandler private final String _query; - public IncludeAttributes(Attributes attributes, String requestURI, String contextPath, String pathInContext, String query) + public IncludeAttributes(Attributes attributes, Request baseRequest, ContextHandler.Context sourceContext, ServletPathMapping sourceMapping, String requestURI, String pathInContext, String query) { super(attributes); + _baseRequest = baseRequest; + _sourceMapping = sourceMapping; _requestURI = requestURI; - _contextPath = contextPath; + _sourceContext = sourceContext; _pathInContext = pathInContext; _query = query; } + ContextHandler.Context getSourceContext() + { + return _sourceContext; + } + + ServletPathMapping getSourceMapping() + { + return _sourceMapping; + } + @Override public Object getAttribute(String key) { @@ -371,17 +388,26 @@ public class Dispatcher implements RequestDispatcher switch (key) { case INCLUDE_PATH_INFO: - return _servletPathMapping == null ? _pathInContext : _servletPathMapping.getPathInfo(); + { + ServletPathMapping mapping = _baseRequest.getServletPathMapping(); + return mapping == null ? _pathInContext : mapping.getPathInfo(); + } case INCLUDE_SERVLET_PATH: - return _servletPathMapping == null ? null : _servletPathMapping.getServletPath(); + { + ServletPathMapping mapping = _baseRequest.getServletPathMapping(); + return mapping == null ? null : mapping.getServletPath(); + } case INCLUDE_CONTEXT_PATH: - return _contextPath; + { + ContextHandler.Context context = _baseRequest.getContext(); + return context == null ? null : context.getContextHandler().getContextPathEncoded(); + } case INCLUDE_QUERY_STRING: return _query; case INCLUDE_REQUEST_URI: return _requestURI; case INCLUDE_MAPPING: - return _servletPathMapping; + return _baseRequest.getServletPathMapping(); default: break; } @@ -416,12 +442,9 @@ public class Dispatcher implements RequestDispatcher @Override public void setAttribute(String key, Object value) { - if (_servletPathMapping == null && _named == null && INCLUDE_MAPPING.equals(key)) - _servletPathMapping = (ServletPathMapping)value; - else - // Allow any attribute to be set, even if a reserved name. If a reserved - // name is set here, it will be revealed after the include is complete. - _attributes.setAttribute(key, value); + // Allow any attribute to be set, even if a reserved name. If a reserved + // name is set here, it will be revealed after the include is complete. + _attributes.setAttribute(key, value); } @Override diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index 8f39b9a7900..e1c00421666 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -107,14 +107,16 @@ import org.slf4j.LoggerFactory; * request object to be as lightweight as possible and not actually implement any significant behavior. For example *
    * - *
  • The {@link Request#getContextPath()} method will return null, until the request has been passed to a {@link ContextHandler} which matches the - * {@link Request#getPathInfo()} with a context path and calls {@link Request#setContextPath(String)} as a result.
  • + *
  • the {@link Request#getContextPath()} method will return null, until the request has been passed to a {@link ContextHandler} which matches the + * {@link Request#getPathInfo()} with a context path and calls {@link Request#setContext(Context,String)} as a result. For + * some dispatch types (ie include and named dispatch) the context path may not reflect the {@link ServletContext} set + * by {@link Request#setContext(Context, String)}.
  • * *
  • the HTTP session methods will all return null sessions until such time as a request has been passed to a * {@link org.eclipse.jetty.server.session.SessionHandler} which checks for session cookies and enables the ability to create new sessions.
  • * - *
  • The {@link Request#getServletPath()} method will return null until the request has been passed to a org.eclipse.jetty.servlet.ServletHandler - * and the pathInfo matched against the servlet URL patterns and {@link Request#setServletPath(String)} called as a result.
  • + *
  • The {@link Request#getServletPath()} method will return "" until the request has been passed to a org.eclipse.jetty.servlet.ServletHandler + * and the pathInfo matched against the servlet URL patterns and {@link Request#setServletPathMapping(ServletPathMapping)} called as a result.
  • *
* *

@@ -198,9 +200,7 @@ public class Request implements HttpServletRequest private HttpFields _trailers; private HttpURI _uri; private String _method; - private String _contextPath; - private String _servletPath; - private String _pathInfo; + private String _pathInContext; private ServletPathMapping _servletPathMapping; private boolean _secure; private String _asyncNotSupportedSource = null; @@ -777,7 +777,44 @@ public class Request implements HttpServletRequest @Override public String getContextPath() { - return _contextPath; + // The context path returned is normally for the current context. Except during a cross context + // INCLUDE dispatch, in which case this method returns the context path of the source context, + // which we recover from the IncludeAttributes wrapper. + Context context; + if (_dispatcherType == DispatcherType.INCLUDE) + { + Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class); + context = (include == null) ? _context : include.getSourceContext(); + } + else + { + context = _context; + } + + if (context == null) + return null; + + // For some reason the spec requires the context path to be encoded (unlike getServletPath). + String contextPath = context.getContextHandler().getContextPathEncoded(); + + // For the root context, the spec requires that the empty string is returned instead of the leading '/' + // which is included in the pathInContext + if (URIUtil.SLASH.equals(contextPath)) + return ""; + return contextPath; + } + + /** Get the path in the context. + * + * The path relative to the context path, analogous to {@link #getServletPath()} + {@link #getPathInfo()}. + * If no context is set, then the path in context is the full path. + * @return The decoded part of the {@link #getRequestURI()} path after any {@link #getContextPath()} + * up to any {@link #getQueryString()}, excluding path parameters. + * @see #setContext(Context, String) + */ + public String getPathInContext() + { + return _pathInContext; } @Override @@ -1048,15 +1085,20 @@ public class Request implements HttpServletRequest @Override public String getPathInfo() { - return _pathInfo; + // The pathInfo returned is normally for the current servlet. Except during an + // INCLUDE dispatch, in which case this method returns the pathInfo of the source servlet, + // which we recover from the IncludeAttributes wrapper. + ServletPathMapping mapping = findServletPathMapping(); + return mapping == null ? _pathInContext : mapping.getPathInfo(); } @Override public String getPathTranslated() { - if (_pathInfo == null || _context == null) + String pathInfo = getPathInfo(); + if (pathInfo == null || _context == null) return null; - return _context.getRealPath(_pathInfo); + return _context.getRealPath(pathInfo); } @Override @@ -1207,7 +1249,7 @@ public class Request implements HttpServletRequest // handle relative path if (!path.startsWith("/")) { - String relTo = URIUtil.addPaths(_servletPath, _pathInfo); + String relTo = _pathInContext; int slash = relTo.lastIndexOf("/"); if (slash > 1) relTo = relTo.substring(0, slash + 1); @@ -1335,9 +1377,11 @@ public class Request implements HttpServletRequest @Override public String getServletPath() { - if (_servletPath == null) - _servletPath = ""; - return _servletPath; + // The servletPath returned is normally for the current servlet. Except during an + // INCLUDE dispatch, in which case this method returns the servletPath of the source servlet, + // which we recover from the IncludeAttributes wrapper. + ServletPathMapping mapping = findServletPathMapping(); + return mapping == null ? "" : mapping.getServletPath(); } public ServletResponse getServletResponse() @@ -1678,10 +1722,10 @@ public class Request implements HttpServletRequest if (path == null || path.isEmpty()) { - setPathInfo(encoded == null ? "" : encoded); + _pathInContext = encoded == null ? "" : encoded; throw new BadMessageException(400, "Bad URI"); } - setPathInfo(path); + _pathInContext = path; } public org.eclipse.jetty.http.MetaData.Request getMetaData() @@ -1740,13 +1784,12 @@ public class Request implements HttpServletRequest } _contentType = null; _characterEncoding = null; - _contextPath = null; + _pathInContext = null; if (_cookies != null) _cookies.reset(); _cookiesExtracted = false; _context = null; _newContext = false; - _pathInfo = null; _queryEncoding = null; _requestedSessionId = null; _requestedSessionIdFromCookie = false; @@ -1754,7 +1797,6 @@ public class Request implements HttpServletRequest _session = null; _sessionHandler = null; _scope = null; - _servletPath = null; _timeStamp = 0; _queryParameters = null; _contentParameters = null; @@ -1831,6 +1873,13 @@ public class Request implements HttpServletRequest } } + /** + * Set the attributes for the request. + * + * @param attributes The attributes, which must be a {@link org.eclipse.jetty.util.Attributes.Wrapper} + * for which {@link Attributes#unwrap(Attributes)} will return the + * original {@link ServletAttributes}. + */ public void setAttributes(Attributes attributes) { _attributes = attributes; @@ -1864,7 +1913,7 @@ public class Request implements HttpServletRequest // attributes there, under any other wrappers. ((ServletAttributes)baseAttributes).setAsyncAttributes(getRequestURI(), getContextPath(), - getPathInfo(), // TODO change to pathInContext when cheaply available + getPathInContext(), getServletPathMapping(), getQueryString()); } @@ -1955,25 +2004,24 @@ public class Request implements HttpServletRequest } /** - * Set request context + * Set request context and path in the context. * * @param context context object + * @param pathInContext the part of the URI path that is withing the context. + * For servlets, this is equal to servletPath + pathInfo */ - public void setContext(Context context) + public void setContext(Context context, String pathInContext) { _newContext = _context != context; - if (context == null) - _context = null; - else - { - _context = context; + _context = context; + _pathInContext = pathInContext; + if (context != null) _errorContext = context; - } } /** * @return True if this is the first call of takeNewContext() since the last - * {@link #setContext(org.eclipse.jetty.server.handler.ContextHandler.Context)} call. + * {@link #setContext(org.eclipse.jetty.server.handler.ContextHandler.Context, String)} call. */ public boolean takeNewContext() { @@ -1982,17 +2030,6 @@ public class Request implements HttpServletRequest return nc; } - /** - * Sets the "context path" for this request - * - * @param contextPath the context path for this request - * @see HttpServletRequest#getContextPath() - */ - public void setContextPath(String contextPath) - { - _contextPath = contextPath; - } - /** * @param cookies The cookies to set. */ @@ -2026,14 +2063,6 @@ public class Request implements HttpServletRequest return HttpMethod.HEAD.is(getMethod()); } - /** - * @param pathInfo The pathInfo to set. - */ - public void setPathInfo(String pathInfo) - { - _pathInfo = pathInfo; - } - /** * Set the character encoding used for the query string. This call will effect the return of getQueryString and getParamaters. It must be called before any * getParameter methods. @@ -2071,14 +2100,6 @@ public class Request implements HttpServletRequest _requestedSessionIdFromCookie = requestedSessionIdCookie; } - /** - * @param servletPath The servletPath to set. - */ - public void setServletPath(String servletPath) - { - _servletPath = servletPath; - } - /** * @param session The session to set. */ @@ -2347,32 +2368,48 @@ public class Request implements HttpServletRequest /** * Set the servletPathMapping, the servletPath and the pathInfo. - * TODO remove the side effect on servletPath and pathInfo by removing those fields. * @param servletPathMapping The mapping used to return from {@link #getHttpServletMapping()} */ public void setServletPathMapping(ServletPathMapping servletPathMapping) { _servletPathMapping = servletPathMapping; - if (servletPathMapping == null) - { - // TODO reset the servletPath and pathInfo, but currently cannot do that - // as we don't know the pathInContext. - } - else - { - _servletPath = servletPathMapping.getServletPath(); - _pathInfo = servletPathMapping.getPathInfo(); - } } + /** + * @return The mapping for the current target servlet, regardless of dispatch type. + */ public ServletPathMapping getServletPathMapping() { return _servletPathMapping; } + /** + * @return The mapping for the target servlet reported by the {@link #getServletPath()} and + * {@link #getPathInfo()} methods. For {@link DispatcherType#INCLUDE} dispatches, this + * method returns the mapping of the source servlet, otherwise it returns the mapping of + * the target servlet. + */ + ServletPathMapping findServletPathMapping() + { + ServletPathMapping mapping; + if (_dispatcherType == DispatcherType.INCLUDE) + { + Dispatcher.IncludeAttributes include = Attributes.unwrap(_attributes, Dispatcher.IncludeAttributes.class); + mapping = (include == null) ? _servletPathMapping : include.getSourceMapping(); + } + else + { + mapping = _servletPathMapping; + } + return mapping; + } + @Override public HttpServletMapping getHttpServletMapping() { - return _servletPathMapping; + // The mapping returned is normally for the current servlet. Except during an + // INCLUDE dispatch, in which case this method returns the mapping of the source servlet, + // which we recover from the IncludeAttributes wrapper. + return findServletPathMapping(); } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java index dc2808e0b1f..923a96153f7 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java @@ -335,7 +335,7 @@ public class Response implements HttpServletResponse return url; if (request.getServerPort() != port) return url; - if (!path.startsWith(request.getContextPath())) //TODO the root context path is "", with which every non null string starts + if (request.getContext() != null && !path.startsWith(request.getContextPath())) return url; } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java index 01cad1a17a6..8cd868c9045 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/Server.java @@ -619,7 +619,7 @@ public class Server extends HandlerWrapper implements Attributes baseRequest.mergeQueryParameters(oldUri.getQuery(), baseRequest.getQueryString()); } - baseRequest.setPathInfo(baseRequest.getHttpURI().getDecodedPath()); + baseRequest.setContext(null, baseRequest.getHttpURI().getDecodedPath()); handleAsync(channel, event, baseRequest); } finally diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java index c7b839ee995..9be5f2edc94 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/BufferedResponseHandler.java @@ -40,7 +40,6 @@ import org.eclipse.jetty.util.Callback; import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.IteratingCallback; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.URIUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,7 +103,7 @@ public class BufferedResponseHandler extends HandlerWrapper public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { final ServletContext context = baseRequest.getServletContext(); - final String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo()); + final String path = baseRequest.getPathInContext(); LOG.debug("{} handle {} in {}", this, baseRequest, context); HttpOutput out = baseRequest.getResponse().getHttpOutput(); diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index 73f2a9bf188..9bcfaa22a63 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -1150,13 +1150,11 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu if (LOG.isDebugEnabled()) LOG.debug("scope {}|{}|{} @ {}", baseRequest.getContextPath(), baseRequest.getServletPath(), baseRequest.getPathInfo(), this); + final Thread currentThread = Thread.currentThread(); + final ClassLoader oldClassloader = currentThread.getContextClassLoader(); Context oldContext; - String oldContextPath = null; - String oldServletPath = null; - String oldPathInfo = null; - ClassLoader oldClassloader = null; - Thread currentThread = null; - String pathInfo = target; + String oldPathInContext = null; + String pathInContext = target; DispatcherType dispatch = baseRequest.getDispatcherType(); @@ -1177,47 +1175,31 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu { if (_contextPath.length() > 1) target = target.substring(_contextPath.length()); - pathInfo = target; + pathInContext = target; } else if (_contextPath.length() == 1) { target = URIUtil.SLASH; - pathInfo = URIUtil.SLASH; + pathInContext = URIUtil.SLASH; } else { target = URIUtil.SLASH; - pathInfo = null; + pathInContext = null; } } - - // Set the classloader - if (_classLoader != null) - { - currentThread = Thread.currentThread(); - oldClassloader = currentThread.getContextClassLoader(); - currentThread.setContextClassLoader(_classLoader); - } } + if (_classLoader != null) + currentThread.setContextClassLoader(_classLoader); + try { - oldContextPath = baseRequest.getContextPath(); - oldServletPath = baseRequest.getServletPath(); - oldPathInfo = baseRequest.getPathInfo(); + oldPathInContext = baseRequest.getPathInContext(); // Update the paths - baseRequest.setContext(_scontext); + baseRequest.setContext(_scontext, pathInContext); __context.set(_scontext); - if (!DispatcherType.INCLUDE.equals(dispatch) && target.startsWith("/")) - { - if (_contextPath.length() == 1) - baseRequest.setContextPath(""); - else - baseRequest.setContextPath(getContextPathEncoded()); - baseRequest.setServletPath(null); - baseRequest.setPathInfo(pathInfo); - } if (oldContext != _scontext) enterScope(baseRequest, dispatch); @@ -1234,17 +1216,12 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu exitScope(baseRequest); // reset the classloader - if (_classLoader != null && currentThread != null) - { + if (_classLoader != null) currentThread.setContextClassLoader(oldClassloader); - } // reset the context and servlet path. - baseRequest.setContext(oldContext); + baseRequest.setContext(oldContext, oldPathInContext); __context.set(oldContext); - baseRequest.setContextPath(oldContextPath); - baseRequest.setServletPath(oldServletPath); - baseRequest.setPathInfo(oldPathInfo); } } } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index 8f678f31dd0..87130148d22 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -45,7 +45,6 @@ import org.eclipse.jetty.server.handler.HandlerWrapper; import org.eclipse.jetty.util.IncludeExclude; import org.eclipse.jetty.util.RegexSet; import org.eclipse.jetty.util.StringUtil; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.compression.DeflaterPool; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -582,7 +581,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { final ServletContext context = baseRequest.getServletContext(); - final String path = context == null ? baseRequest.getRequestURI() : URIUtil.addPaths(baseRequest.getServletPath(), baseRequest.getPathInfo()); + final String path = baseRequest.getPathInContext(); LOG.debug("{} handle {} in {}", this, baseRequest, context); if (!_dispatchers.contains(baseRequest.getDispatcherType())) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index a38af17e0cb..aad28e07814 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -43,7 +43,6 @@ import javax.servlet.MultipartConfigElement; import javax.servlet.ServletException; import javax.servlet.ServletInputStream; import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletMapping; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -2175,29 +2174,4 @@ public class RequestTest return null; } } - - private class PathMappingHandler extends AbstractHandler - { - private ServletPathSpec _spec; - private String _servletPath; - private String _servletName; - - public PathMappingHandler(ServletPathSpec spec, String servletPath, String servletName) - { - _spec = spec; - _servletPath = servletPath; - _servletName = servletName; - } - - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException - { - ((Request)request).setHandled(true); - baseRequest.setServletPath(_servletPath); - if (_servletName != null) - baseRequest.setUserIdentityScope(new TestUserIdentityScope(null, null, _servletName)); - HttpServletMapping mapping = baseRequest.getHttpServletMapping(); - response.getWriter().println(mapping); - } - } } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java index 3f3b3220061..a7fa0bed681 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResponseTest.java @@ -95,7 +95,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; // @checkstyle-disable-check : AvoidEscapedUnicodeCharactersCheck public class ResponseTest { - static final InetSocketAddress LOCALADDRESS; static @@ -353,7 +352,7 @@ public class ResponseTest ContextHandler context = new ContextHandler(); context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1"); context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2"); - response.getHttpChannel().getRequest().setContext(context.getServletContext()); + response.getHttpChannel().getRequest().setContext(context.getServletContext(), "/"); response.setLocale(java.util.Locale.ITALIAN); assertNull(response.getContentType()); @@ -376,7 +375,7 @@ public class ResponseTest ContextHandler context = new ContextHandler(); context.addLocaleEncoding(Locale.ENGLISH.toString(), "ISO-8859-1"); context.addLocaleEncoding(Locale.ITALIAN.toString(), "ISO-8859-2"); - response.getHttpChannel().getRequest().setContext(context.getServletContext()); + response.getHttpChannel().getRequest().setContext(context.getServletContext(), "/"); response.setLocale(java.util.Locale.ITALIAN); @@ -425,46 +424,46 @@ public class ResponseTest //test setting the default response character encoding Response response = getResponse(); - _channel.getRequest().setContext(handler.getServletContext()); + response.getHttpChannel().getRequest().setContext(handler.getServletContext(), "/"); assertThat("utf-16", Matchers.equalTo(response.getCharacterEncoding())); - _channel.getRequest().setContext(null); + _channel.getRequest().setContext(null, "/"); response.recycle(); //test that explicit overrides default response = getResponse(); - _channel.getRequest().setContext(handler.getServletContext()); + _channel.getRequest().setContext(handler.getServletContext(), "/"); response.setCharacterEncoding("ascii"); assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding())); //getWriter should not change explicit character encoding response.getWriter(); assertThat("ascii", Matchers.equalTo(response.getCharacterEncoding())); - _channel.getRequest().setContext(null); + _channel.getRequest().setContext(null, "/"); response.recycle(); //test that assumed overrides default response = getResponse(); - _channel.getRequest().setContext(handler.getServletContext()); + _channel.getRequest().setContext(handler.getServletContext(), "/"); response.setContentType("application/json"); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); response.getWriter(); //getWriter should not have modified character encoding assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); - _channel.getRequest().setContext(null); + _channel.getRequest().setContext(null, "/"); response.recycle(); //test that inferred overrides default response = getResponse(); - _channel.getRequest().setContext(handler.getServletContext()); + _channel.getRequest().setContext(handler.getServletContext(), "/"); response.setContentType("application/xhtml+xml"); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); //getWriter should not have modified character encoding response.getWriter(); assertThat("utf-8", Matchers.equalTo(response.getCharacterEncoding())); - _channel.getRequest().setContext(null); + _channel.getRequest().setContext(null, "/"); response.recycle(); //test that without a default or any content type, use iso-8859-1 @@ -488,7 +487,7 @@ public class ResponseTest _server.start(); Response response = getResponse(); - response.getHttpChannel().getRequest().setContext(handler.getServletContext()); + response.getHttpChannel().getRequest().setContext(handler.getServletContext(), "/"); response.setContentType("text/html"); assertEquals("iso-8859-1", response.getCharacterEncoding()); @@ -859,10 +858,11 @@ public class ResponseTest @Test public void testEncodeRedirect() { + ContextHandler context = new ContextHandler("/path"); Response response = getResponse(); Request request = response.getHttpChannel().getRequest(); request.setHttpURI(HttpURI.build(request.getHttpURI()).host("myhost").port(8888)); - request.setContextPath("/path"); + request.setContext(context.getServletContext(), "/info"); assertEquals("http://myhost:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/path/info;param?query=0&more=1#target")); @@ -893,7 +893,23 @@ public class ResponseTest assertEquals("http://myhost/path/info;param?query=0&more=1#target", response.encodeURL("http://myhost/path/info;param?query=0&more=1#target")); assertEquals("http://myhost:8888/other/info;param?query=0&more=1#target", response.encodeURL("http://myhost:8888/other/info;param?query=0&more=1#target")); - request.setContextPath(""); + context = new ContextHandler("/"); + request.setContext(context.getServletContext(), "/"); + assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888")); + assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888")); + assertEquals("mailto:/foo", response.encodeURL("mailto:/foo")); + assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/")); + assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888/;jsessionid=7777")); + assertEquals("http://myhost:8888/;param;jsessionid=12345?query=0&more=1#target", response.encodeURL("http://myhost:8888/;param?query=0&more=1#target")); + assertEquals("http://other:8888/path/info;param?query=0&more=1#target", response.encodeURL("http://other:8888/path/info;param?query=0&more=1#target")); + handler.setCheckingRemoteSessionIdEncoding(false); + assertEquals("/foo;jsessionid=12345", response.encodeURL("/foo")); + assertEquals("/;jsessionid=12345", response.encodeURL("/")); + assertEquals("/foo.html;jsessionid=12345#target", response.encodeURL("/foo.html#target")); + assertEquals(";jsessionid=12345", response.encodeURL("")); + + request.setContext(null, "/"); + handler.setCheckingRemoteSessionIdEncoding(true); assertEquals("http://myhost:8888/;jsessionid=12345", response.encodeURL("http://myhost:8888")); assertEquals("https://myhost:8888/;jsessionid=12345", response.encodeURL("https://myhost:8888")); assertEquals("mailto:/foo", response.encodeURL("mailto:/foo")); @@ -937,6 +953,7 @@ public class ResponseTest {"http://somehost.com/other/location", "http://somehost.com/other/location"}, }; + ContextHandler context = new ContextHandler("/path"); int[] ports = new int[]{8080, 80}; String[] hosts = new String[]{null, "myhost", "192.168.0.1", "0::1"}; for (int port : ports) @@ -956,7 +973,7 @@ public class ResponseTest if (host != null) uri.host(host).port(port); request.setHttpURI(uri); - request.setContextPath("/path"); + request.setContext(context.getServletContext(), "/info"); request.setRequestedSessionId("12345"); request.setRequestedSessionIdFromCookie(i > 2); SessionHandler handler = new SessionHandler(); @@ -980,6 +997,7 @@ public class ResponseTest .replace("@HOST@", host == null ? request.getLocalAddr() : (host.contains(":") ? ("[" + host + "]") : host)) .replace("@PORT@", host == null ? ":8888" : (port == 80 ? "" : (":" + port))); assertEquals(expected, location, "test-" + i + " " + host + ":" + port); + request.setContext(null, "/info"); } } } @@ -1116,7 +1134,7 @@ public class ResponseTest { Response response = getResponse(); TestServletContextHandler context = new TestServletContextHandler(); - _channel.getRequest().setContext(context.getServletContext()); + _channel.getRequest().setContext(context.getServletContext(), "/"); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, HttpCookie.SameSite.STRICT); Cookie cookie = new Cookie("name", "value"); cookie.setDomain("domain"); @@ -1282,7 +1300,7 @@ public class ResponseTest Response response = getResponse(); TestServletContextHandler context = new TestServletContextHandler(); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); - _channel.getRequest().setContext(context.getServletContext()); + _channel.getRequest().setContext(context.getServletContext(), "/"); //replace with no prior does an add response.replaceCookie(new HttpCookie("Foo", "123456")); String set = response.getHttpFields().get("Set-Cookie"); @@ -1324,7 +1342,7 @@ public class ResponseTest Response response = getResponse(); TestServletContextHandler context = new TestServletContextHandler(); context.setAttribute(HttpCookie.SAME_SITE_DEFAULT_ATTRIBUTE, "LAX"); - _channel.getRequest().setContext(context.getServletContext()); + _channel.getRequest().setContext(context.getServletContext(), "/"); response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456"); response.replaceCookie(new HttpCookie("Foo", "value")); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java index 69c2340937f..207183631be 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/ServletHandler.java @@ -37,7 +37,6 @@ import java.util.stream.Stream; import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterChain; -import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -66,7 +65,6 @@ import org.eclipse.jetty.util.ArrayUtil; import org.eclipse.jetty.util.LazyList; import org.eclipse.jetty.util.MultiException; import org.eclipse.jetty.util.MultiMap; -import org.eclipse.jetty.util.URIUtil; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.component.DumpableCollection; @@ -431,10 +429,7 @@ public class ServletHandler extends ScopedHandler if (servletPathMapping != null) { // Setting the servletPathMapping also provides the servletPath and pathInfo - if (DispatcherType.INCLUDE.equals(type)) - baseRequest.setAttribute(RequestDispatcher.INCLUDE_MAPPING, servletPathMapping); - else - baseRequest.setServletPathMapping(servletPathMapping); + baseRequest.setServletPathMapping(servletPathMapping); } } @@ -1405,7 +1400,7 @@ public class ServletHandler extends ScopedHandler if (LOG.isDebugEnabled()) LOG.debug("Not Found {}", request.getRequestURI()); if (getHandler() != null) - nextHandle(URIUtil.addPaths(request.getServletPath(), request.getPathInfo()), baseRequest, request, response); + nextHandle(baseRequest.getPathInContext(), baseRequest, request, response); } protected synchronized boolean containsFilterHolder(FilterHolder holder) diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java index 18bd366cfeb..7defbffa40a 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Attributes.java @@ -43,6 +43,10 @@ public interface Attributes void clearAttributes(); + /** Unwrap all {@link Wrapper}s of the attributes + * @param attributes The attributes to unwrap, which may be a {@link Wrapper}. + * @return The core attributes + */ static Attributes unwrap(Attributes attributes) { while (attributes instanceof Wrapper) @@ -52,6 +56,26 @@ public interface Attributes return attributes; } + /** Unwrap attributes to a specific attribute {@link Wrapper}. + * @param attributes The attributes to unwrap, which may be a {@link Wrapper} + * @param target The target {@link Wrapper} class. + * @param The type of the target {@link Wrapper}. + * @return The outermost {@link Wrapper} of the matching type of null if not found. + */ + static T unwrap(Attributes attributes, Class target) + { + while (attributes instanceof Wrapper) + { + if (target.isAssignableFrom(attributes.getClass())) + return (T)attributes; + attributes = ((Wrapper)attributes).getAttributes(); + } + return null; + } + + /** + * A Wrapper of attributes + */ abstract class Wrapper implements Attributes { protected final Attributes _attributes;