diff --git a/History.md b/History.md index 7892f691..3e8a6c9d 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,7 @@ +## 2.0.0 (UNRELEASED) + +- Support Jakarta Servlet API 5.0 (JEE 9) + ## 1.3.0 (UNRELEASED) - Support Javax Servlet API 4.0 (JEE 8) diff --git a/README.md b/README.md index 5bb322ac..ace32d0d 100644 --- a/README.md +++ b/README.md @@ -291,7 +291,7 @@ Or the equivalent of doing `bundle exec rackup ...` if you're using Bundler : ## Logging JRuby-Rack sets up a delegate logger for Rails that sends logging output to -`javax.servlet.ServletContext#log` by default. If you wish to use a different +`jakarta.servlet.ServletContext#log` by default. If you wish to use a different logging system, configure `jruby.rack.logging` as follows: - `servlet_context` (default): Sends log messages to the servlet context. diff --git a/pom.xml b/pom.xml index 9c557ea0..e49ed6b6 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.jruby.rack jruby-rack - 1.3.0-SNAPSHOT + 2.0.0-SNAPSHOT JRuby-Rack https://github.com/jruby/jruby-rack/ @@ -91,13 +91,13 @@ jakarta.servlet jakarta.servlet-api - 4.0.4 + 5.0.0 provided jakarta.servlet.jsp jakarta.servlet.jsp-api - 2.3.6 + 3.0.0 provided @@ -136,16 +136,10 @@ ${spring.version} test - - org.springframework - spring-test - ${spring.version} - test - jakarta.el jakarta.el-api - 3.0.3 + 4.0.0 test diff --git a/src/main/java/org/jruby/rack/AbstractFilter.java b/src/main/java/org/jruby/rack/AbstractFilter.java index acc98c5d..c4ac7f59 100644 --- a/src/main/java/org/jruby/rack/AbstractFilter.java +++ b/src/main/java/org/jruby/rack/AbstractFilter.java @@ -8,14 +8,14 @@ package org.jruby.rack; import java.io.IOException; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jruby.rack.servlet.RequestCapture; import org.jruby.rack.servlet.ResponseCapture; import org.jruby.rack.servlet.ServletRackEnvironment; diff --git a/src/main/java/org/jruby/rack/AbstractServlet.java b/src/main/java/org/jruby/rack/AbstractServlet.java index e52b37c3..fc4d1e9e 100644 --- a/src/main/java/org/jruby/rack/AbstractServlet.java +++ b/src/main/java/org/jruby/rack/AbstractServlet.java @@ -8,13 +8,13 @@ package org.jruby.rack; import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.jruby.rack.servlet.ServletRackEnvironment; import org.jruby.rack.servlet.ServletRackResponseEnvironment; diff --git a/src/main/java/org/jruby/rack/RackDispatcher.java b/src/main/java/org/jruby/rack/RackDispatcher.java index b187bcc1..38e1a573 100644 --- a/src/main/java/org/jruby/rack/RackDispatcher.java +++ b/src/main/java/org/jruby/rack/RackDispatcher.java @@ -8,7 +8,8 @@ package org.jruby.rack; import java.io.IOException; -import javax.servlet.ServletException; + +import jakarta.servlet.ServletException; /** * diff --git a/src/main/java/org/jruby/rack/RackEnvironment.java b/src/main/java/org/jruby/rack/RackEnvironment.java index 9d95fbb4..03c1dbf5 100644 --- a/src/main/java/org/jruby/rack/RackEnvironment.java +++ b/src/main/java/org/jruby/rack/RackEnvironment.java @@ -13,7 +13,7 @@ /** * Represent a Rack environment (that will most likely by wrapping a - * {@link javax.servlet.http.HttpServletRequest}). + * {@link jakarta.servlet.http.HttpServletRequest}). * Allows Rack applications to be loaded outside of JEE servlet environments. * * @see org.jruby.rack.servlet.ServletRackEnvironment @@ -37,7 +37,7 @@ public interface RackEnvironment { // The following methods are specific to the rack environment /** - * @see javax.servlet.ServletRequest#getInputStream() + * @see jakarta.servlet.ServletRequest#getInputStream() * @return the input as a stream * @throws IOException if there's an IO exception */ @@ -53,107 +53,107 @@ public interface RackEnvironment { // The following methods are usually inherited from the servlet request /** - * @see javax.servlet.http.HttpServletRequest#getPathInfo() + * @see jakarta.servlet.http.HttpServletRequest#getPathInfo() * @return the request path info */ String getPathInfo(); /** * Request URI should include the query string if available. - * @see javax.servlet.http.HttpServletRequest#getRequestURI() + * @see jakarta.servlet.http.HttpServletRequest#getRequestURI() * @return the request URI */ String getRequestURI(); /** - * @see javax.servlet.http.HttpServletRequest#getAttributeNames() + * @see jakarta.servlet.http.HttpServletRequest#getAttributeNames() * @return an enumeration of all attribute names */ Enumeration getAttributeNames(); /** - * @see javax.servlet.http.HttpServletRequest#getAttribute(String) + * @see jakarta.servlet.http.HttpServletRequest#getAttribute(String) * @param key the attribute key * @return the attribute value */ Object getAttribute(String key); /** - * @see javax.servlet.http.HttpServletRequest#setAttribute(String, Object) + * @see jakarta.servlet.http.HttpServletRequest#setAttribute(String, Object) * @param key the key * @param value the value */ void setAttribute(String key, Object value); /** - * @see javax.servlet.http.HttpServletRequest#getHeaderNames() + * @see jakarta.servlet.http.HttpServletRequest#getHeaderNames() * @return an enumeration of all header names */ Enumeration getHeaderNames(); /** - * @see javax.servlet.http.HttpServletRequest#getHeader(String) + * @see jakarta.servlet.http.HttpServletRequest#getHeader(String) * @param name the header name * @return the header value */ String getHeader(String name); /** - * @see javax.servlet.http.HttpServletRequest#getScheme() + * @see jakarta.servlet.http.HttpServletRequest#getScheme() * @return the request scheme */ String getScheme(); /** - * @see javax.servlet.http.HttpServletRequest#getContentType() + * @see jakarta.servlet.http.HttpServletRequest#getContentType() * @return the content type */ String getContentType(); /** - * @see javax.servlet.http.HttpServletRequest#getContentLength() + * @see jakarta.servlet.http.HttpServletRequest#getContentLength() * @return the content length */ int getContentLength(); /** - * @see javax.servlet.http.HttpServletRequest#getMethod() + * @see jakarta.servlet.http.HttpServletRequest#getMethod() * @return the request method */ String getMethod(); /** - * @see javax.servlet.http.HttpServletRequest#getQueryString() + * @see jakarta.servlet.http.HttpServletRequest#getQueryString() * @return the query string */ String getQueryString(); /** - * @see javax.servlet.http.HttpServletRequest#getServerName() + * @see jakarta.servlet.http.HttpServletRequest#getServerName() * @return the server name */ String getServerName(); /** - * @see javax.servlet.http.HttpServletRequest#getServerPort() + * @see jakarta.servlet.http.HttpServletRequest#getServerPort() * @return the server port */ int getServerPort(); /** - * @see javax.servlet.ServletRequest#getRemoteHost() + * @see jakarta.servlet.ServletRequest#getRemoteHost() * @return the remote host */ String getRemoteHost(); /** - * @see javax.servlet.ServletRequest#getRemoteAddr() + * @see jakarta.servlet.ServletRequest#getRemoteAddr() * @return the remote address */ String getRemoteAddr(); /** - * @see javax.servlet.http.HttpServletRequest#getRemoteUser() + * @see jakarta.servlet.http.HttpServletRequest#getRemoteUser() * @return the remote user */ String getRemoteUser(); diff --git a/src/main/java/org/jruby/rack/RackFilter.java b/src/main/java/org/jruby/rack/RackFilter.java index b4494cac..1df1f910 100644 --- a/src/main/java/org/jruby/rack/RackFilter.java +++ b/src/main/java/org/jruby/rack/RackFilter.java @@ -9,13 +9,13 @@ import java.io.IOException; import java.net.MalformedURLException; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import org.jruby.rack.servlet.RequestCapture; import org.jruby.rack.servlet.ResponseCapture; import org.jruby.rack.servlet.ServletRackContext; diff --git a/src/main/java/org/jruby/rack/RackResponseEnvironment.java b/src/main/java/org/jruby/rack/RackResponseEnvironment.java index 48da1b58..57eb093a 100644 --- a/src/main/java/org/jruby/rack/RackResponseEnvironment.java +++ b/src/main/java/org/jruby/rack/RackResponseEnvironment.java @@ -16,8 +16,8 @@ * handle and return the Rack response) interface. * It is likely to be (only) implemented as a HTTP servlet response. * - * @see javax.servlet.ServletResponse - * @see javax.servlet.http.HttpServletResponse + * @see jakarta.servlet.ServletResponse + * @see jakarta.servlet.http.HttpServletResponse * @see RackResponse * * @author nicksieger @@ -26,30 +26,30 @@ public interface RackResponseEnvironment { /** * @return whether the underlying response has been committed. - * @see javax.servlet.ServletResponse#isCommitted() + * @see jakarta.servlet.ServletResponse#isCommitted() */ boolean isCommitted(); /** * Reset the response (buffer) so we can begin a new response. - * @see javax.servlet.ServletResponse#reset() + * @see jakarta.servlet.ServletResponse#reset() */ void reset(); /** - * @see javax.servlet.ServletResponse#setContentType(String) + * @see jakarta.servlet.ServletResponse#setContentType(String) * @param type the content type */ void setContentType(String type) ; /** - * @see javax.servlet.ServletResponse#setContentLength(int) + * @see jakarta.servlet.ServletResponse#setContentLength(int) * @param length the content length */ void setContentLength(int length) ; /** - * @see javax.servlet.ServletResponse#setCharacterEncoding(String) + * @see jakarta.servlet.ServletResponse#setCharacterEncoding(String) * @param charset the charset */ void setCharacterEncoding(String charset) ; @@ -107,14 +107,14 @@ public interface RackResponseEnvironment { void sendError(int code) throws IOException ; /** - * @see javax.servlet.ServletResponse#getOutputStream() + * @see jakarta.servlet.ServletResponse#getOutputStream() * @return the output stream * @throws IOException if there's an IO exception */ OutputStream getOutputStream() throws IOException ; /** - * @see javax.servlet.ServletResponse#getWriter() + * @see jakarta.servlet.ServletResponse#getWriter() * @return the writer * @throws IOException if there's an IO exception */ diff --git a/src/main/java/org/jruby/rack/RackServlet.java b/src/main/java/org/jruby/rack/RackServlet.java index e5bf4944..a67c78ff 100644 --- a/src/main/java/org/jruby/rack/RackServlet.java +++ b/src/main/java/org/jruby/rack/RackServlet.java @@ -7,7 +7,7 @@ package org.jruby.rack; -import javax.servlet.ServletConfig; +import jakarta.servlet.ServletConfig; @SuppressWarnings("serial") public class RackServlet extends AbstractServlet { diff --git a/src/main/java/org/jruby/rack/RackServletContextListener.java b/src/main/java/org/jruby/rack/RackServletContextListener.java index 2fee5092..df470f5d 100644 --- a/src/main/java/org/jruby/rack/RackServletContextListener.java +++ b/src/main/java/org/jruby/rack/RackServletContextListener.java @@ -7,10 +7,9 @@ package org.jruby.rack; -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletContextEvent; +import jakarta.servlet.ServletContextListener; import org.jruby.rack.servlet.DefaultServletRackContext; import org.jruby.rack.servlet.ServletRackConfig; import org.jruby.rack.servlet.ServletRackContext; diff --git a/src/main/java/org/jruby/rack/RackTag.java b/src/main/java/org/jruby/rack/RackTag.java index 73b4d8e1..30ae25a1 100644 --- a/src/main/java/org/jruby/rack/RackTag.java +++ b/src/main/java/org/jruby/rack/RackTag.java @@ -7,12 +7,11 @@ package org.jruby.rack; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TagSupport; - +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.jsp.JspException; +import jakarta.servlet.jsp.tagext.TagSupport; import org.jruby.rack.servlet.ServletRackEnvironment; @SuppressWarnings("serial") diff --git a/src/main/java/org/jruby/rack/UnmappedRackFilter.java b/src/main/java/org/jruby/rack/UnmappedRackFilter.java index da64da12..b2f9f4bb 100644 --- a/src/main/java/org/jruby/rack/UnmappedRackFilter.java +++ b/src/main/java/org/jruby/rack/UnmappedRackFilter.java @@ -14,11 +14,11 @@ import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletResponse; import org.jruby.rack.servlet.RequestCapture; import org.jruby.rack.servlet.ResponseCapture; diff --git a/src/main/java/org/jruby/rack/embed/Servlet.java b/src/main/java/org/jruby/rack/embed/Servlet.java index 69974537..1f0ad34c 100644 --- a/src/main/java/org/jruby/rack/embed/Servlet.java +++ b/src/main/java/org/jruby/rack/embed/Servlet.java @@ -7,8 +7,7 @@ */ package org.jruby.rack.embed; -import javax.servlet.ServletConfig; - +import jakarta.servlet.ServletConfig; import org.jruby.rack.AbstractServlet; import org.jruby.rack.RackContext; import org.jruby.rack.RackDispatcher; diff --git a/src/main/java/org/jruby/rack/ext/Logger.java b/src/main/java/org/jruby/rack/ext/Logger.java index 6d63e90c..a96e570c 100644 --- a/src/main/java/org/jruby/rack/ext/Logger.java +++ b/src/main/java/org/jruby/rack/ext/Logger.java @@ -23,8 +23,7 @@ */ package org.jruby.rack.ext; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import org.jruby.Ruby; import org.jruby.RubyClass; import org.jruby.RubyException; diff --git a/src/main/java/org/jruby/rack/logging/CommonsLoggingLogger.java b/src/main/java/org/jruby/rack/logging/CommonsLoggingLogger.java index 458aecc2..c7299e8c 100644 --- a/src/main/java/org/jruby/rack/logging/CommonsLoggingLogger.java +++ b/src/main/java/org/jruby/rack/logging/CommonsLoggingLogger.java @@ -33,7 +33,7 @@ public void setLoggerName(String loggerName) { @Override public boolean isEnabled(Level level) { - if ( level == null ) return logger.isInfoEnabled(); // TODO ???! + if ( level == null ) return logger.isInfoEnabled(); switch ( level ) { case DEBUG: return logger.isDebugEnabled(); case INFO: return logger.isInfoEnabled(); diff --git a/src/main/java/org/jruby/rack/logging/ServletContextLogger.java b/src/main/java/org/jruby/rack/logging/ServletContextLogger.java index 9ea6a3bc..3c02b668 100644 --- a/src/main/java/org/jruby/rack/logging/ServletContextLogger.java +++ b/src/main/java/org/jruby/rack/logging/ServletContextLogger.java @@ -7,8 +7,7 @@ */ package org.jruby.rack.logging; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import org.jruby.rack.RackLogger; public class ServletContextLogger extends RackLogger.Base { diff --git a/src/main/java/org/jruby/rack/servlet/DefaultServletRackContext.java b/src/main/java/org/jruby/rack/servlet/DefaultServletRackContext.java index e1077566..1053cd89 100644 --- a/src/main/java/org/jruby/rack/servlet/DefaultServletRackContext.java +++ b/src/main/java/org/jruby/rack/servlet/DefaultServletRackContext.java @@ -14,17 +14,17 @@ import java.util.EventListener; import java.util.Map; import java.util.Set; -import javax.servlet.Filter; -import javax.servlet.FilterRegistration; -import javax.servlet.RequestDispatcher; -import javax.servlet.Servlet; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRegistration; -import javax.servlet.SessionCookieConfig; -import javax.servlet.SessionTrackingMode; -import javax.servlet.descriptor.JspConfigDescriptor; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRegistration; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.SessionTrackingMode; +import jakarta.servlet.descriptor.JspConfigDescriptor; import org.jruby.rack.RackApplicationFactory; import org.jruby.rack.RackConfig; import org.jruby.rack.RackLogger; @@ -198,13 +198,12 @@ public String getServletContextName() { return context.getServletContextName(); } + // RackLogger @Override @Deprecated public void log(Exception e, String msg) { logger.log(msg, e); } - // RackLogger - @Override public boolean isEnabled(Level level) { return logger.isEnabled(level); diff --git a/src/main/java/org/jruby/rack/servlet/HttpUtils.java b/src/main/java/org/jruby/rack/servlet/HttpUtils.java new file mode 100644 index 00000000..edd7ad71 --- /dev/null +++ b/src/main/java/org/jruby/rack/servlet/HttpUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 1997-2018 Oracle and/or its affiliates and others. + * All rights reserved. + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jruby.rack.servlet; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; + +/** + * @deprecated As of Java(tm) Servlet API 2.3. These methods were only useful with the default encoding and have been + * moved to the request interfaces. + * @implNote Propagated to jruby-rack from old Javax Servlet API <= 4.0. + * + */ +@Deprecated +public class HttpUtils { + + private HttpUtils() {} + + /** + * Parses a query string passed from the client to the server and builds a HashMap object with + * key-value pairs. The query string should be in the form of a string packaged by the GET or POST method, that is, + * it should have key-value pairs in the form key=value, with each pair separated from the next by a & + * character. + * + *

+ * A key can appear more than once in the query string with different values. However, the key appears only once in + * the HashMap, with its value being an array of strings containing the multiple values sent by the query string. + * + *

+ * The keys and values in the HashMap are stored in their decoded form, so any + characters are converted to + * spaces, and characters sent in hexadecimal notation (like %xx) are converted to ASCII characters. + * + * @param s a string containing the query to be parsed + * + * @return a Map object built from the parsed key-value pairs + * + * @exception IllegalArgumentException if the query string is invalid + */ + public static Map parseQueryString(String s) { + + String valArray[]; + + if (s == null) { + throw new IllegalArgumentException(); + } + + Map ht = new HashMap<>(); + StringBuilder sb = new StringBuilder(); + StringTokenizer st = new StringTokenizer(s, "&"); + while (st.hasMoreTokens()) { + String pair = st.nextToken(); + int pos = pair.indexOf('='); + if (pos == -1) { + // XXX + // should give more detail about the illegal argument + throw new IllegalArgumentException(); + } + String key = parseName(pair.substring(0, pos), sb); + String val = parseName(pair.substring(pos + 1), sb); + if (ht.containsKey(key)) { + String oldVals[] = ht.get(key); + valArray = new String[oldVals.length + 1]; + System.arraycopy(oldVals, 0, valArray, 0, oldVals.length); + valArray[oldVals.length] = val; + } else { + valArray = new String[1]; + valArray[0] = val; + } + ht.put(key, valArray); + } + + return ht; + } + + /* + * Parse a name in the query string. + */ + private static String parseName(String s, StringBuilder sb) { + sb.setLength(0); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '+': + sb.append(' '); + break; + case '%': + try { + sb.append((char) Integer.parseInt(s.substring(i + 1, i + 3), 16)); + i += 2; + } catch (NumberFormatException e) { + // XXX + // need to be more specific about illegal arg + throw new IllegalArgumentException(); + } catch (StringIndexOutOfBoundsException e) { + String rest = s.substring(i); + sb.append(rest); + if (rest.length() == 2) + i++; + } + + break; + default: + sb.append(c); + break; + } + } + + return sb.toString(); + } + +} diff --git a/src/main/java/org/jruby/rack/servlet/RequestCapture.java b/src/main/java/org/jruby/rack/servlet/RequestCapture.java index 598c36f2..816ed203 100644 --- a/src/main/java/org/jruby/rack/servlet/RequestCapture.java +++ b/src/main/java/org/jruby/rack/servlet/RequestCapture.java @@ -16,9 +16,10 @@ import java.util.HashMap; import java.util.Iterator; import java.util.Map; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; /** * Request wrapper passed to filter chain. diff --git a/src/main/java/org/jruby/rack/servlet/ResponseCapture.java b/src/main/java/org/jruby/rack/servlet/ResponseCapture.java index 73b3efb6..7224d5d3 100644 --- a/src/main/java/org/jruby/rack/servlet/ResponseCapture.java +++ b/src/main/java/org/jruby/rack/servlet/ResponseCapture.java @@ -12,11 +12,12 @@ import java.io.PrintWriter; import java.util.Collection; import java.util.Collections; -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; /** * Response wrapper passed to filter chain. @@ -80,15 +81,6 @@ public void setStatus(int status) { } } - @SuppressWarnings("deprecation") - @Override - @Deprecated - public void setStatus(int status, String message) { - if ( handleStatus(status, false) ) { - super.setStatus(status, message); - } - } - @Override public void sendError(int status) throws IOException { if ( handleStatus(status, true) ) { diff --git a/src/main/java/org/jruby/rack/servlet/RewindableInputStream.java b/src/main/java/org/jruby/rack/servlet/RewindableInputStream.java index c20aa38d..7367b7b8 100644 --- a/src/main/java/org/jruby/rack/servlet/RewindableInputStream.java +++ b/src/main/java/org/jruby/rack/servlet/RewindableInputStream.java @@ -12,8 +12,9 @@ import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; /** * Originally inspired by Kirk's RewindableInputStream ... diff --git a/src/main/java/org/jruby/rack/servlet/ServletRackConfig.java b/src/main/java/org/jruby/rack/servlet/ServletRackConfig.java index 3fa00530..87a3e377 100644 --- a/src/main/java/org/jruby/rack/servlet/ServletRackConfig.java +++ b/src/main/java/org/jruby/rack/servlet/ServletRackConfig.java @@ -7,8 +7,7 @@ package org.jruby.rack.servlet; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import org.jruby.rack.DefaultRackConfig; import org.jruby.rack.RackLogger; import org.jruby.rack.logging.ServletContextLogger; diff --git a/src/main/java/org/jruby/rack/servlet/ServletRackContext.java b/src/main/java/org/jruby/rack/servlet/ServletRackContext.java index bb67a335..f9a39d28 100644 --- a/src/main/java/org/jruby/rack/servlet/ServletRackContext.java +++ b/src/main/java/org/jruby/rack/servlet/ServletRackContext.java @@ -7,8 +7,7 @@ package org.jruby.rack.servlet; -import javax.servlet.ServletContext; - +import jakarta.servlet.ServletContext; import org.jruby.rack.RackApplicationFactory; import org.jruby.rack.RackContext; diff --git a/src/main/java/org/jruby/rack/servlet/ServletRackEnvironment.java b/src/main/java/org/jruby/rack/servlet/ServletRackEnvironment.java index 7725a80c..86a2ea45 100644 --- a/src/main/java/org/jruby/rack/servlet/ServletRackEnvironment.java +++ b/src/main/java/org/jruby/rack/servlet/ServletRackEnvironment.java @@ -8,11 +8,11 @@ package org.jruby.rack.servlet; import java.io.IOException; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; import org.jruby.rack.RackContext; import org.jruby.rack.RackEnvironment; import org.jruby.rack.ext.Input; diff --git a/src/main/java/org/jruby/rack/servlet/ServletRackIncludedResponse.java b/src/main/java/org/jruby/rack/servlet/ServletRackIncludedResponse.java index 545de948..b53cc67c 100644 --- a/src/main/java/org/jruby/rack/servlet/ServletRackIncludedResponse.java +++ b/src/main/java/org/jruby/rack/servlet/ServletRackIncludedResponse.java @@ -13,11 +13,12 @@ import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletResponse; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; /** * Response wrapper used to buffer the output of a server-side include. diff --git a/src/main/java/org/jruby/rack/servlet/ServletRackResponseEnvironment.java b/src/main/java/org/jruby/rack/servlet/ServletRackResponseEnvironment.java index a071d7f3..e24a880a 100644 --- a/src/main/java/org/jruby/rack/servlet/ServletRackResponseEnvironment.java +++ b/src/main/java/org/jruby/rack/servlet/ServletRackResponseEnvironment.java @@ -8,9 +8,9 @@ package org.jruby.rack.servlet; import java.io.IOException; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; import org.jruby.rack.DefaultErrorApplication; import org.jruby.rack.RackResponse; import org.jruby.rack.RackResponseEnvironment; diff --git a/src/main/ruby/jruby/rack/servlet_ext.rb b/src/main/ruby/jruby/rack/servlet_ext.rb index 4df3b62e..bb1e2e41 100644 --- a/src/main/ruby/jruby/rack/servlet_ext.rb +++ b/src/main/ruby/jruby/rack/servlet_ext.rb @@ -9,7 +9,7 @@ # Ruby-friendly extensions to the Servlet API. -module Java::JavaxServlet::ServletContext +module Java::JakartaServlet::ServletContext # Fetch an attribute from the servlet context. def [](key) getAttribute(key.to_s) @@ -38,7 +38,7 @@ def each end end -module Java::JavaxServlet::ServletRequest +module Java::JakartaServlet::ServletRequest # Fetch an attribute from the servlet request. def [](key) getAttribute(key.to_s) @@ -67,7 +67,7 @@ def each end end -module Java::JavaxServletHttp::HttpSession +module Java::JakartaServletHttp::HttpSession # Fetch an attribute from the session. def [](key) getAttribute(key.to_s) diff --git a/src/main/ruby/jruby/rack/version.rb b/src/main/ruby/jruby/rack/version.rb index d77aa9b6..f3f34c54 100644 --- a/src/main/ruby/jruby/rack/version.rb +++ b/src/main/ruby/jruby/rack/version.rb @@ -8,6 +8,6 @@ module JRuby module Rack - VERSION = '1.3.0.SNAPSHOT' + VERSION = '2.0.0.SNAPSHOT' end end diff --git a/src/main/ruby/rack/handler/servlet/default_env.rb b/src/main/ruby/rack/handler/servlet/default_env.rb index bf6786be..0ecd202e 100644 --- a/src/main/ruby/rack/handler/servlet/default_env.rb +++ b/src/main/ruby/rack/handler/servlet/default_env.rb @@ -231,14 +231,14 @@ def load_variable(env, key) def load_builtin(env, key) case key - when 'rack.version' then env[key] = ::Rack::VERSION - when 'rack.multithread' then env[key] = true - when 'rack.multiprocess' then env[key] = false - when 'rack.run_once' then env[key] = false - when 'rack.hijack?' then env[key] = false - when 'rack.input' then + when 'rack.version' then env[key] = ::Rack::VERSION + when 'rack.multithread' then env[key] = true + when 'rack.multiprocess' then env[key] = false + when 'rack.run_once' then env[key] = false + when 'rack.hijack?' then env[key] = false + when 'rack.input' then env[key] = @servlet_env ? JRuby::Rack::Input.new(@servlet_env) : nil - when 'rack.errors' then context = rack_context + when 'rack.errors' then context = rack_context env[key] = context ? JRuby::Rack::ServletLog.new(context) : nil when 'rack.url_scheme' env[key] = scheme = @servlet_env ? @servlet_env.getScheme : nil @@ -246,9 +246,9 @@ def load_builtin(env, key) scheme when 'java.servlet_request' then env[key] = servlet_request when 'java.servlet_response' then env[key] = servlet_response - when 'java.servlet_context' then env[key] = servlet_context - when 'jruby.rack.context' then env[key] = rack_context - when 'jruby.rack.version' then env[key] = JRuby::Rack::VERSION + when 'java.servlet_context' then env[key] = @servlet_env.servlet_context + when 'jruby.rack.context' then env[key] = rack_context + when 'jruby.rack.version' then env[key] = JRuby::Rack::VERSION else nil end @@ -274,20 +274,6 @@ def servlet_response @servlet_env.respond_to?(:response) ? @servlet_env.response : @servlet_env end - def servlet_context - if @servlet_env.respond_to?(:servlet_context) # @since Servlet 3.0 - @servlet_env.servlet_context # ServletRequest#getServletContext() - else - if @servlet_env.respond_to?(:context) && - @servlet_env.context.is_a?(javax.servlet.ServletContext) - @servlet_env.context - else - JRuby::Rack.context || - ( servlet_request ? servlet_request.servlet_context : nil ) - end - end - end - TRANSIENT_KEYS = [ 'rack.input', 'rack.errors', 'java.servlet_request', 'java.servlet_response', 'java.servlet_context', 'jruby.rack.context' diff --git a/src/main/ruby/rack/handler/servlet/servlet_env.rb b/src/main/ruby/rack/handler/servlet/servlet_env.rb index efc469d4..0a605b03 100644 --- a/src/main/ruby/rack/handler/servlet/servlet_env.rb +++ b/src/main/ruby/rack/handler/servlet/servlet_env.rb @@ -58,7 +58,7 @@ def load_env_key(env, key) end # Load parameters into the (Rack) env from the Servlet API. - # using javax.servlet.http.HttpServletRequest#getParameterMap + # using Java::JakartaServletHttp::HttpServletRequest#getParameterMap def load_parameters get_only = ! POST_PARAM_METHODS.include?( @servlet_env.getMethod ) # we only need to really do this for POSTs but we'll handle all @@ -153,7 +153,7 @@ def store_parameter(key, val, hash) COOKIE_HASH = "rack.request.cookie_hash".freeze # Load cookies into the (Rack) env from the Servlet API. - # using javax.servlet.http.HttpServletRequest#getCookies + # using Java::JakartaServletHttp::HttpServletRequest#getCookies def load_cookies cookie_hash = {} (@servlet_env.getCookies || []).each do |cookie| @@ -184,7 +184,7 @@ def query_values(key) end def parse_query_string - Java::JavaxServletHttp::HttpUtils.parseQueryString(query_string) + Java::OrgJrubyRackServlet::HttpUtils.parseQueryString(query_string) end def mark_parameter_error(msg) diff --git a/src/spec/java/org/jruby/rack/fake/FakeJspWriter.java b/src/spec/java/org/jruby/rack/fake/FakeJspWriter.java index d35f9518..a9f8e960 100644 --- a/src/spec/java/org/jruby/rack/fake/FakeJspWriter.java +++ b/src/spec/java/org/jruby/rack/fake/FakeJspWriter.java @@ -8,7 +8,8 @@ package org.jruby.rack.fake; import java.io.IOException; -import javax.servlet.jsp.JspWriter; + +import jakarta.servlet.jsp.JspWriter; /** * Currently only used as a mock for testing. diff --git a/src/spec/java/org/jruby/rack/fake/FakePageContext.java b/src/spec/java/org/jruby/rack/fake/FakePageContext.java index 65c08749..a2425d50 100644 --- a/src/spec/java/org/jruby/rack/fake/FakePageContext.java +++ b/src/spec/java/org/jruby/rack/fake/FakePageContext.java @@ -9,30 +9,30 @@ import java.io.IOException; import java.util.Enumeration; -import javax.el.ELContext; -import javax.servlet.Servlet; -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.el.ExpressionEvaluator; -import javax.servlet.jsp.el.VariableResolver; + +import jakarta.el.ELContext; +import jakarta.servlet.Servlet; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.jsp.JspWriter; +import jakarta.servlet.jsp.PageContext; +import jakarta.servlet.jsp.el.ExpressionEvaluator; +import jakarta.servlet.jsp.el.VariableResolver; /** * Currently only used as a mock for testing. */ -@SuppressWarnings("deprecation") public class FakePageContext extends PageContext { - private ServletContext context; - private ServletRequest request; - private ServletResponse response; - private JspWriter out; + private final ServletContext context; + private final ServletRequest request; + private final ServletResponse response; + private final JspWriter out; public FakePageContext(ServletContext context, HttpServletRequest request, HttpServletResponse response, JspWriter out) { this.context = context; @@ -160,12 +160,12 @@ public void include(String arg0, boolean arg1) throws ServletException, IOExcept throw new UnsupportedOperationException("Not supported yet."); } - @Override + @Override @Deprecated public ExpressionEvaluator getExpressionEvaluator() { throw new UnsupportedOperationException("Not supported yet."); } - @Override + @Override @Deprecated public VariableResolver getVariableResolver() { throw new UnsupportedOperationException("Not supported yet."); } diff --git a/src/spec/java/org/jruby/rack/mock/RackLoggingMockServletContext.java b/src/spec/java/org/jruby/rack/mock/RackLoggingMockServletContext.java index b937c51b..46a0cff7 100644 --- a/src/spec/java/org/jruby/rack/mock/RackLoggingMockServletContext.java +++ b/src/spec/java/org/jruby/rack/mock/RackLoggingMockServletContext.java @@ -53,12 +53,6 @@ public void log(String message) { logger.log(message); } - @Override - @SuppressWarnings("deprecation") - public void log(Exception ex, String message) { - logger.log(message, ex); - } - @Override public void log(String message, Throwable ex) { logger.log(message, ex); diff --git a/src/spec/java/org/jruby/rack/mock/fail/FailingHttpServletResponse.java b/src/spec/java/org/jruby/rack/mock/fail/FailingHttpServletResponse.java index 64986d01..4c4ce08d 100644 --- a/src/spec/java/org/jruby/rack/mock/fail/FailingHttpServletResponse.java +++ b/src/spec/java/org/jruby/rack/mock/fail/FailingHttpServletResponse.java @@ -24,8 +24,8 @@ package org.jruby.rack.mock.fail; import java.io.IOException; -import javax.servlet.ServletOutputStream; +import jakarta.servlet.ServletOutputStream; import org.springframework.mock.web.MockHttpServletResponse; /** diff --git a/src/spec/java/org/springframework/mock/web/DelegatingServletInputStream.java b/src/spec/java/org/springframework/mock/web/DelegatingServletInputStream.java new file mode 100644 index 00000000..6b5ffb9b --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/DelegatingServletInputStream.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Delegating implementation of {@link jakarta.servlet.ServletInputStream}. + * + *

Used by {@link MockHttpServletRequest}; typically not directly + * used for testing application controllers. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see MockHttpServletRequest + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class DelegatingServletInputStream extends ServletInputStream { + + private final InputStream sourceStream; + + private boolean finished = false; + + + /** + * Create a DelegatingServletInputStream for the given source stream. + * @param sourceStream the source stream (never {@code null}) + */ + public DelegatingServletInputStream(InputStream sourceStream) { + Assert.notNull(sourceStream, "Source InputStream must not be null"); + this.sourceStream = sourceStream; + } + + /** + * Return the underlying source stream (never {@code null}). + */ + public final InputStream getSourceStream() { + return this.sourceStream; + } + + + @Override + public int read() throws IOException { + int data = this.sourceStream.read(); + if (data == -1) { + this.finished = true; + } + return data; + } + + @Override + public int available() throws IOException { + return this.sourceStream.available(); + } + + @Override + public void close() throws IOException { + super.close(); + this.sourceStream.close(); + } + + @Override + public boolean isFinished() { + return this.finished; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/DelegatingServletOutputStream.java b/src/spec/java/org/springframework/mock/web/DelegatingServletOutputStream.java new file mode 100644 index 00000000..0c0aa114 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/DelegatingServletOutputStream.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import org.springframework.util.Assert; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Delegating implementation of {@link jakarta.servlet.ServletOutputStream}. + * + *

Used by {@link MockHttpServletResponse}; typically not directly + * used for testing application controllers. + * + * @author Juergen Hoeller + * @since 1.0.2 + * @see MockHttpServletResponse + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class DelegatingServletOutputStream extends ServletOutputStream { + + private final OutputStream targetStream; + + + /** + * Create a DelegatingServletOutputStream for the given target stream. + * @param targetStream the target stream (never {@code null}) + */ + public DelegatingServletOutputStream(OutputStream targetStream) { + Assert.notNull(targetStream, "Target OutputStream must not be null"); + this.targetStream = targetStream; + } + + /** + * Return the underlying target stream (never {@code null}). + */ + public final OutputStream getTargetStream() { + return this.targetStream; + } + + + @Override + public void write(int b) throws IOException { + this.targetStream.write(b); + } + + @Override + public void flush() throws IOException { + super.flush(); + this.targetStream.flush(); + } + + @Override + public void close() throws IOException { + super.close(); + this.targetStream.close(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/HeaderValueHolder.java b/src/spec/java/org/springframework/mock/web/HeaderValueHolder.java new file mode 100644 index 00000000..60751cdb --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/HeaderValueHolder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import java.util.*; + +/** + * Internal helper class that serves as a value holder for request headers. + * + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0.1 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +class HeaderValueHolder { + + private final List values = new LinkedList<>(); + + + void setValue(@Nullable Object value) { + this.values.clear(); + if (value != null) { + this.values.add(value); + } + } + + void addValue(Object value) { + this.values.add(value); + } + + void addValues(Collection values) { + this.values.addAll(values); + } + + void addValueArray(Object values) { + CollectionUtils.mergeArrayIntoCollection(values, this.values); + } + + List getValues() { + return Collections.unmodifiableList(this.values); + } + + List getStringValues() { + List stringList = new ArrayList<>(this.values.size()); + for (Object value : this.values) { + stringList.add(value.toString()); + } + return Collections.unmodifiableList(stringList); + } + + @Nullable + Object getValue() { + return (!this.values.isEmpty() ? this.values.get(0) : null); + } + + @Nullable + String getStringValue() { + return (!this.values.isEmpty() ? String.valueOf(this.values.get(0)) : null); + } + + @Override + public String toString() { + return this.values.toString(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockAsyncContext.java b/src/spec/java/org/springframework/mock/web/MockAsyncContext.java new file mode 100644 index 00000000..599ef279 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockAsyncContext.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.BeanUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.web.util.WebUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Mock implementation of the {@link AsyncContext} interface. + * + * @author Rossen Stoyanchev + * @since 3.2 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockAsyncContext implements AsyncContext { + + private final HttpServletRequest request; + + @Nullable + private final HttpServletResponse response; + + private final List listeners = new ArrayList<>(); + + @Nullable + private String dispatchedPath; + + private long timeout = 10 * 1000L; + + private final List dispatchHandlers = new ArrayList<>(); + + + public MockAsyncContext(ServletRequest request, @Nullable ServletResponse response) { + this.request = (HttpServletRequest) request; + this.response = (HttpServletResponse) response; + } + + + public void addDispatchHandler(Runnable handler) { + Assert.notNull(handler, "Dispatch handler must not be null"); + synchronized (this) { + if (this.dispatchedPath == null) { + this.dispatchHandlers.add(handler); + } + else { + handler.run(); + } + } + } + + @Override + public ServletRequest getRequest() { + return this.request; + } + + @Override + @Nullable + public ServletResponse getResponse() { + return this.response; + } + + @Override + public boolean hasOriginalRequestAndResponse() { + return (this.request instanceof MockHttpServletRequest && this.response instanceof MockHttpServletResponse); + } + + @Override + public void dispatch() { + dispatch(this.request.getRequestURI()); + } + + @Override + public void dispatch(String path) { + dispatch(null, path); + } + + @Override + public void dispatch(@Nullable ServletContext context, String path) { + synchronized (this) { + this.dispatchedPath = path; + this.dispatchHandlers.forEach(Runnable::run); + } + } + + @Nullable + public String getDispatchedPath() { + return this.dispatchedPath; + } + + @Override + public void complete() { + MockHttpServletRequest mockRequest = WebUtils.getNativeRequest(this.request, MockHttpServletRequest.class); + if (mockRequest != null) { + mockRequest.setAsyncStarted(false); + } + for (AsyncListener listener : this.listeners) { + try { + listener.onComplete(new AsyncEvent(this, this.request, this.response)); + } + catch (IOException ex) { + throw new IllegalStateException("AsyncListener failure", ex); + } + } + } + + @Override + public void start(Runnable runnable) { + runnable.run(); + } + + @Override + public void addListener(AsyncListener listener) { + this.listeners.add(listener); + } + + @Override + public void addListener(AsyncListener listener, ServletRequest request, ServletResponse response) { + this.listeners.add(listener); + } + + public List getListeners() { + return this.listeners; + } + + @Override + public T createListener(Class clazz) throws ServletException { + return BeanUtils.instantiateClass(clazz); + } + + /** + * By default this is set to 10000 (10 seconds) even though the Servlet API + * specifies a default async request timeout of 30 seconds. Keep in mind the + * timeout could further be impacted by global configuration through the MVC + * Java config or the XML namespace, as well as be overridden per request on + * {@link org.springframework.web.context.request.async.DeferredResult DeferredResult} + * or on + * {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter SseEmitter}. + * @param timeout the timeout value to use. + * @see AsyncContext#setTimeout(long) + */ + @Override + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + @Override + public long getTimeout() { + return this.timeout; + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockCookie.java b/src/spec/java/org/springframework/mock/web/MockCookie.java new file mode 100644 index 00000000..2c0c363a --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockCookie.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.http.Cookie; +import org.springframework.core.style.ToStringCreator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.time.DateTimeException; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +/** + * Extension of {@code Cookie} with extra attributes, as defined in + * RFC 6265. + * + * @author Vedran Pavic + * @author Juergen Hoeller + * @author Sam Brannen + * @since 5.1 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockCookie extends Cookie { + + private static final long serialVersionUID = 4312531139502726325L; + + + @Nullable + private ZonedDateTime expires; + + @Nullable + private String sameSite; + + + /** + * Construct a new {@link MockCookie} with the supplied name and value. + * @param name the name + * @param value the value + * @see Cookie#Cookie(String, String) + */ + public MockCookie(String name, String value) { + super(name, value); + } + + /** + * Set the "Expires" attribute for this cookie. + * @since 5.1.11 + */ + public void setExpires(@Nullable ZonedDateTime expires) { + this.expires = expires; + } + + /** + * Get the "Expires" attribute for this cookie. + * @return the "Expires" attribute for this cookie, or {@code null} if not set + * @since 5.1.11 + */ + @Nullable + public ZonedDateTime getExpires() { + return this.expires; + } + + /** + * Set the "SameSite" attribute for this cookie. + *

This limits the scope of the cookie such that it will only be attached + * to same-site requests if the supplied value is {@code "Strict"} or cross-site + * requests if the supplied value is {@code "Lax"}. + * @see RFC6265 bis + */ + public void setSameSite(@Nullable String sameSite) { + this.sameSite = sameSite; + } + + /** + * Get the "SameSite" attribute for this cookie. + * @return the "SameSite" attribute for this cookie, or {@code null} if not set + */ + @Nullable + public String getSameSite() { + return this.sameSite; + } + + + /** + * Factory method that parses the value of the supplied "Set-Cookie" header. + * @param setCookieHeader the "Set-Cookie" value; never {@code null} or empty + * @return the created cookie + */ + public static MockCookie parse(String setCookieHeader) { + Assert.notNull(setCookieHeader, "Set-Cookie header must not be null"); + String[] cookieParts = setCookieHeader.split("\\s*=\\s*", 2); + Assert.isTrue(cookieParts.length == 2, () -> "Invalid Set-Cookie header '" + setCookieHeader + "'"); + + String name = cookieParts[0]; + String[] valueAndAttributes = cookieParts[1].split("\\s*;\\s*", 2); + String value = valueAndAttributes[0]; + String[] attributes = + (valueAndAttributes.length > 1 ? valueAndAttributes[1].split("\\s*;\\s*") : new String[0]); + + MockCookie cookie = new MockCookie(name, value); + for (String attribute : attributes) { + if (StringUtils.startsWithIgnoreCase(attribute, "Domain")) { + cookie.setDomain(extractAttributeValue(attribute, setCookieHeader)); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "Max-Age")) { + cookie.setMaxAge(Integer.parseInt(extractAttributeValue(attribute, setCookieHeader))); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "Expires")) { + try { + cookie.setExpires(ZonedDateTime.parse(extractAttributeValue(attribute, setCookieHeader), + DateTimeFormatter.RFC_1123_DATE_TIME)); + } + catch (DateTimeException ex) { + // ignore invalid date formats + } + } + else if (StringUtils.startsWithIgnoreCase(attribute, "Path")) { + cookie.setPath(extractAttributeValue(attribute, setCookieHeader)); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "Secure")) { + cookie.setSecure(true); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "HttpOnly")) { + cookie.setHttpOnly(true); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "SameSite")) { + cookie.setSameSite(extractAttributeValue(attribute, setCookieHeader)); + } + else if (StringUtils.startsWithIgnoreCase(attribute, "Comment")) { + cookie.setComment(extractAttributeValue(attribute, setCookieHeader)); + } + } + return cookie; + } + + private static String extractAttributeValue(String attribute, String header) { + String[] nameAndValue = attribute.split("="); + Assert.isTrue(nameAndValue.length == 2, + () -> "No value in attribute '" + nameAndValue[0] + "' for Set-Cookie header '" + header + "'"); + return nameAndValue[1]; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("name", getName()) + .append("value", getValue()) + .append("Path", getPath()) + .append("Domain", getDomain()) + .append("Version", getVersion()) + .append("Comment", getComment()) + .append("Secure", getSecure()) + .append("HttpOnly", isHttpOnly()) + .append("SameSite", this.sameSite) + .append("Max-Age", getMaxAge()) + .append("Expires", (this.expires != null ? + DateTimeFormatter.RFC_1123_DATE_TIME.format(this.expires) : null)) + .toString(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockHttpServletRequest.java b/src/spec/java/org/springframework/mock/web/MockHttpServletRequest.java new file mode 100644 index 00000000..a6faaa00 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -0,0 +1,1361 @@ +/* + * Copyright 2002-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.*; +import jakarta.servlet.http.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.*; + +import java.io.*; +import java.security.Principal; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Mock implementation of the {@link jakarta.servlet.http.HttpServletRequest} interface. + * + *

The default, preferred {@link Locale} for the server mocked by this request + * is {@link Locale#ENGLISH}. This value can be changed via {@link #addPreferredLocale} + * or {@link #setPreferredLocales}. + * + *

As of Spring 5.0, this set of mocks is designed on a Servlet 4.0 baseline. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Rick Evans + * @author Mark Fisher + * @author Chris Beams + * @author Sam Brannen + * @author Brian Clozel + * @since 1.0.2 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockHttpServletRequest implements HttpServletRequest { + + private static final String HTTP = "http"; + + private static final String HTTPS = "https"; + + private static final String CHARSET_PREFIX = "charset="; + + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + private static final ServletInputStream EMPTY_SERVLET_INPUT_STREAM = + new DelegatingServletInputStream(StreamUtils.emptyInput()); + + private static final BufferedReader EMPTY_BUFFERED_READER = + new BufferedReader(new StringReader("")); + + /** + * Date formats as specified in the HTTP RFC. + * @see Section 7.1.1.1 of RFC 7231 + */ + private static final String[] DATE_FORMATS = new String[] { + "EEE, dd MMM yyyy HH:mm:ss zzz", + "EEE, dd-MMM-yy HH:mm:ss zzz", + "EEE MMM dd HH:mm:ss yyyy" + }; + + + // --------------------------------------------------------------------- + // Public constants + // --------------------------------------------------------------------- + + /** + * The default protocol: 'HTTP/1.1'. + * @since 4.3.7 + */ + public static final String DEFAULT_PROTOCOL = "HTTP/1.1"; + + /** + * The default scheme: 'http'. + * @since 4.3.7 + */ + public static final String DEFAULT_SCHEME = HTTP; + + /** + * The default server address: '127.0.0.1'. + */ + public static final String DEFAULT_SERVER_ADDR = "127.0.0.1"; + + /** + * The default server name: 'localhost'. + */ + public static final String DEFAULT_SERVER_NAME = "localhost"; + + /** + * The default server port: '80'. + */ + public static final int DEFAULT_SERVER_PORT = 80; + + /** + * The default remote address: '127.0.0.1'. + */ + public static final String DEFAULT_REMOTE_ADDR = "127.0.0.1"; + + /** + * The default remote host: 'localhost'. + */ + public static final String DEFAULT_REMOTE_HOST = "localhost"; + + + // --------------------------------------------------------------------- + // Lifecycle properties + // --------------------------------------------------------------------- + + private final ServletContext servletContext; + + private boolean active = true; + + + // --------------------------------------------------------------------- + // ServletRequest properties + // --------------------------------------------------------------------- + + private final Map attributes = new LinkedHashMap<>(); + + @Nullable + private String characterEncoding; + + @Nullable + private byte[] content; + + @Nullable + private String contentType; + + @Nullable + private ServletInputStream inputStream; + + @Nullable + private BufferedReader reader; + + private final Map parameters = new LinkedHashMap<>(16); + + private String protocol = DEFAULT_PROTOCOL; + + private String scheme = DEFAULT_SCHEME; + + private String serverName = DEFAULT_SERVER_NAME; + + private int serverPort = DEFAULT_SERVER_PORT; + + private String remoteAddr = DEFAULT_REMOTE_ADDR; + + private String remoteHost = DEFAULT_REMOTE_HOST; + + /** List of locales in descending order. */ + private final LinkedList locales = new LinkedList<>(); + + private boolean secure = false; + + private int remotePort = DEFAULT_SERVER_PORT; + + private String localName = DEFAULT_SERVER_NAME; + + private String localAddr = DEFAULT_SERVER_ADDR; + + private int localPort = DEFAULT_SERVER_PORT; + + private boolean asyncStarted = false; + + private boolean asyncSupported = false; + + @Nullable + private MockAsyncContext asyncContext; + + private DispatcherType dispatcherType = DispatcherType.REQUEST; + + + // --------------------------------------------------------------------- + // HttpServletRequest properties + // --------------------------------------------------------------------- + + @Nullable + private String authType; + + @Nullable + private Cookie[] cookies; + + private final Map headers = new LinkedCaseInsensitiveMap<>(); + + @Nullable + private String method; + + @Nullable + private String pathInfo; + + private String contextPath = ""; + + @Nullable + private String queryString; + + @Nullable + private String remoteUser; + + private final Set userRoles = new HashSet<>(); + + @Nullable + private Principal userPrincipal; + + @Nullable + private String requestedSessionId; + + @Nullable + private String requestURI; + + private String servletPath = ""; + + @Nullable + private HttpSession session; + + private boolean requestedSessionIdValid = true; + + private boolean requestedSessionIdFromCookie = true; + + private boolean requestedSessionIdFromURL = false; + + private final MultiValueMap parts = new LinkedMultiValueMap<>(); + + + // --------------------------------------------------------------------- + // Constructors + // --------------------------------------------------------------------- + + /** + * Create a new {@code MockHttpServletRequest} with a default + * {@link MockServletContext}. + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest() { + this(null, "", ""); + } + + /** + * Create a new {@code MockHttpServletRequest} with a default + * {@link MockServletContext}. + * @param method the request method (may be {@code null}) + * @param requestURI the request URI (may be {@code null}) + * @see #setMethod + * @see #setRequestURI + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest(@Nullable String method, @Nullable String requestURI) { + this(null, method, requestURI); + } + + /** + * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}. + * @param servletContext the ServletContext that the request runs in + * (may be {@code null} to use a default {@link MockServletContext}) + * @see #MockHttpServletRequest(ServletContext, String, String) + */ + public MockHttpServletRequest(@Nullable ServletContext servletContext) { + this(servletContext, "", ""); + } + + /** + * Create a new {@code MockHttpServletRequest} with the supplied {@link ServletContext}, + * {@code method}, and {@code requestURI}. + *

The preferred locale will be set to {@link Locale#ENGLISH}. + * @param servletContext the ServletContext that the request runs in (may be + * {@code null} to use a default {@link MockServletContext}) + * @param method the request method (may be {@code null}) + * @param requestURI the request URI (may be {@code null}) + * @see #setMethod + * @see #setRequestURI + * @see #setPreferredLocales + * @see MockServletContext + */ + public MockHttpServletRequest(@Nullable ServletContext servletContext, @Nullable String method, @Nullable String requestURI) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.method = method; + this.requestURI = requestURI; + this.locales.add(Locale.ENGLISH); + } + + + // --------------------------------------------------------------------- + // Lifecycle methods + // --------------------------------------------------------------------- + + /** + * Return the ServletContext that this request is associated with. (Not + * available in the standard HttpServletRequest interface for some reason.) + */ + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + /** + * Return whether this request is still active (that is, not completed yet). + */ + public boolean isActive() { + return this.active; + } + + /** + * Mark this request as completed, keeping its state. + */ + public void close() { + this.active = false; + } + + /** + * Invalidate this request, clearing its state. + */ + public void invalidate() { + close(); + clearAttributes(); + } + + /** + * Check whether this request is still active (that is, not completed yet), + * throwing an IllegalStateException if not active anymore. + */ + protected void checkActive() throws IllegalStateException { + Assert.state(this.active, "Request is not active anymore"); + } + + + // --------------------------------------------------------------------- + // ServletRequest interface + // --------------------------------------------------------------------- + + @Override + public Object getAttribute(String name) { + checkActive(); + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + checkActive(); + return Collections.enumeration(new LinkedHashSet<>(this.attributes.keySet())); + } + + @Override + @Nullable + public String getCharacterEncoding() { + return this.characterEncoding; + } + + @Override + public void setCharacterEncoding(@Nullable String characterEncoding) { + this.characterEncoding = characterEncoding; + updateContentTypeHeader(); + } + + private void updateContentTypeHeader() { + if (StringUtils.hasLength(this.contentType)) { + String value = this.contentType; + if (StringUtils.hasLength(this.characterEncoding) && !this.contentType.toLowerCase().contains(CHARSET_PREFIX)) { + value += ';' + CHARSET_PREFIX + this.characterEncoding; + } + doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true); + } + } + + /** + * Set the content of the request body as a byte array. + *

If the supplied byte array represents text such as XML or JSON, the + * {@link #setCharacterEncoding character encoding} should typically be + * set as well. + * @see #setCharacterEncoding(String) + * @see #getContentAsByteArray() + * @see #getContentAsString() + */ + public void setContent(@Nullable byte[] content) { + this.content = content; + this.inputStream = null; + this.reader = null; + } + + /** + * Get the content of the request body as a byte array. + * @return the content as a byte array (potentially {@code null}) + * @since 5.0 + * @see #setContent(byte[]) + * @see #getContentAsString() + */ + @Nullable + public byte[] getContentAsByteArray() { + return this.content; + } + + /** + * Get the content of the request body as a {@code String}, using the configured + * {@linkplain #getCharacterEncoding character encoding}. + * @return the content as a {@code String}, potentially {@code null} + * @throws IllegalStateException if the character encoding has not been set + * @throws UnsupportedEncodingException if the character encoding is not supported + * @since 5.0 + * @see #setContent(byte[]) + * @see #setCharacterEncoding(String) + * @see #getContentAsByteArray() + */ + @Nullable + public String getContentAsString() throws IllegalStateException, UnsupportedEncodingException { + Assert.state(this.characterEncoding != null, + "Cannot get content as a String for a null character encoding. " + + "Consider setting the characterEncoding in the request."); + + if (this.content == null) { + return null; + } + return new String(this.content, this.characterEncoding); + } + + @Override + public int getContentLength() { + return (this.content != null ? this.content.length : -1); + } + + @Override + public long getContentLengthLong() { + return getContentLength(); + } + + public void setContentType(@Nullable String contentType) { + this.contentType = contentType; + if (contentType != null) { + try { + MediaType mediaType = MediaType.parseMediaType(contentType); + if (mediaType.getCharset() != null) { + this.characterEncoding = mediaType.getCharset().name(); + } + } + catch (IllegalArgumentException ex) { + // Try to get charset value anyway + int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); + if (charsetIndex != -1) { + this.characterEncoding = contentType.substring(charsetIndex + CHARSET_PREFIX.length()); + } + } + updateContentTypeHeader(); + } + } + + @Override + @Nullable + public String getContentType() { + return this.contentType; + } + + @Override + public ServletInputStream getInputStream() { + if (this.inputStream != null) { + return this.inputStream; + } + else if (this.reader != null) { + throw new IllegalStateException( + "Cannot call getInputStream() after getReader() has already been called for the current request") ; + } + + this.inputStream = (this.content != null ? + new DelegatingServletInputStream(new ByteArrayInputStream(this.content)) : + EMPTY_SERVLET_INPUT_STREAM); + return this.inputStream; + } + + /** + * Set a single value for the specified HTTP parameter. + *

If there are already one or more values registered for the given + * parameter name, they will be replaced. + */ + public void setParameter(String name, String value) { + setParameter(name, new String[] {value}); + } + + /** + * Set an array of values for the specified HTTP parameter. + *

If there are already one or more values registered for the given + * parameter name, they will be replaced. + */ + public void setParameter(String name, String... values) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.put(name, values); + } + + /** + * Set all provided parameters replacing any existing + * values for the provided parameter names. To add without replacing + * existing values, use {@link #addParameters(Map)}. + */ + public void setParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + params.forEach((key, value) -> { + if (value instanceof String) { + setParameter(key, (String) value); + } + else if (value instanceof String[]) { + setParameter(key, (String[]) value); + } + else { + throw new IllegalArgumentException( + "Parameter map value must be single value " + " or array of type [" + String.class.getName() + "]"); + } + }); + } + + /** + * Add a single value for the specified HTTP parameter. + *

If there are already one or more values registered for the given + * parameter name, the given value will be added to the end of the list. + */ + public void addParameter(String name, @Nullable String value) { + addParameter(name, new String[] {value}); + } + + /** + * Add an array of values for the specified HTTP parameter. + *

If there are already one or more values registered for the given + * parameter name, the given values will be added to the end of the list. + */ + public void addParameter(String name, String... values) { + Assert.notNull(name, "Parameter name must not be null"); + String[] oldArr = this.parameters.get(name); + if (oldArr != null) { + String[] newArr = new String[oldArr.length + values.length]; + System.arraycopy(oldArr, 0, newArr, 0, oldArr.length); + System.arraycopy(values, 0, newArr, oldArr.length, values.length); + this.parameters.put(name, newArr); + } + else { + this.parameters.put(name, values); + } + } + + /** + * Add all provided parameters without replacing any + * existing values. To replace existing values, use + * {@link #setParameters(Map)}. + */ + public void addParameters(Map params) { + Assert.notNull(params, "Parameter map must not be null"); + params.forEach((key, value) -> { + if (value instanceof String) { + addParameter(key, (String) value); + } + else if (value instanceof String[]) { + addParameter(key, (String[]) value); + } + else { + throw new IllegalArgumentException("Parameter map value must be single value " + + " or array of type [" + String.class.getName() + "]"); + } + }); + } + + /** + * Remove already registered values for the specified HTTP parameter, if any. + */ + public void removeParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + this.parameters.remove(name); + } + + /** + * Remove all existing parameters. + */ + public void removeAllParameters() { + this.parameters.clear(); + } + + @Override + @Nullable + public String getParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + String[] arr = this.parameters.get(name); + return (arr != null && arr.length > 0 ? arr[0] : null); + } + + @Override + public Enumeration getParameterNames() { + return Collections.enumeration(this.parameters.keySet()); + } + + @Override + public String[] getParameterValues(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.parameters.get(name); + } + + @Override + public Map getParameterMap() { + return Collections.unmodifiableMap(this.parameters); + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Override + public String getProtocol() { + return this.protocol; + } + + public void setScheme(String scheme) { + this.scheme = scheme; + } + + @Override + public String getScheme() { + return this.scheme; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + @Override + public String getServerName() { + String rawHostHeader = getHeader(HttpHeaders.HOST); + String host = rawHostHeader; + if (host != null) { + host = host.trim(); + if (host.startsWith("[")) { + int indexOfClosingBracket = host.indexOf(']'); + Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); + host = host.substring(0, indexOfClosingBracket + 1); + } + else if (host.contains(":")) { + host = host.substring(0, host.indexOf(':')); + } + return host; + } + + // else + return this.serverName; + } + + public void setServerPort(int serverPort) { + this.serverPort = serverPort; + } + + @Override + public int getServerPort() { + String rawHostHeader = getHeader(HttpHeaders.HOST); + String host = rawHostHeader; + if (host != null) { + host = host.trim(); + int idx; + if (host.startsWith("[")) { + int indexOfClosingBracket = host.indexOf(']'); + Assert.state(indexOfClosingBracket > -1, () -> "Invalid Host header: " + rawHostHeader); + idx = host.indexOf(':', indexOfClosingBracket); + } + else { + idx = host.indexOf(':'); + } + if (idx != -1) { + return Integer.parseInt(host.substring(idx + 1)); + } + } + + // else + return this.serverPort; + } + + @Override + public BufferedReader getReader() throws UnsupportedEncodingException { + if (this.reader != null) { + return this.reader; + } + else if (this.inputStream != null) { + throw new IllegalStateException( + "Cannot call getReader() after getInputStream() has already been called for the current request") ; + } + + if (this.content != null) { + InputStream sourceStream = new ByteArrayInputStream(this.content); + Reader sourceReader = (this.characterEncoding != null) ? + new InputStreamReader(sourceStream, this.characterEncoding) : + new InputStreamReader(sourceStream); + this.reader = new BufferedReader(sourceReader); + } + else { + this.reader = EMPTY_BUFFERED_READER; + } + return this.reader; + } + + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + @Override + public String getRemoteAddr() { + return this.remoteAddr; + } + + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + @Override + public String getRemoteHost() { + return this.remoteHost; + } + + @Override + public void setAttribute(String name, @Nullable Object value) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + @Override + public void removeAttribute(String name) { + checkActive(); + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + /** + * Clear all of this request's attributes. + */ + public void clearAttributes() { + this.attributes.clear(); + } + + /** + * Add a new preferred locale, before any existing locales. + * @see #setPreferredLocales + */ + public void addPreferredLocale(Locale locale) { + Assert.notNull(locale, "Locale must not be null"); + this.locales.addFirst(locale); + updateAcceptLanguageHeader(); + } + + /** + * Set the list of preferred locales, in descending order, effectively replacing + * any existing locales. + * @since 3.2 + * @see #addPreferredLocale + */ + public void setPreferredLocales(List locales) { + Assert.notEmpty(locales, "Locale list must not be empty"); + this.locales.clear(); + this.locales.addAll(locales); + updateAcceptLanguageHeader(); + } + + private void updateAcceptLanguageHeader() { + HttpHeaders headers = new HttpHeaders(); + headers.setAcceptLanguageAsLocales(this.locales); + doAddHeaderValue(HttpHeaders.ACCEPT_LANGUAGE, headers.getFirst(HttpHeaders.ACCEPT_LANGUAGE), true); + } + + /** + * Return the first preferred {@linkplain Locale locale} configured + * in this mock request. + *

If no locales have been explicitly configured, the default, + * preferred {@link Locale} for the server mocked by this + * request is {@link Locale#ENGLISH}. + *

In contrast to the Servlet specification, this mock implementation + * does not take into consideration any locales + * specified via the {@code Accept-Language} header. + * @see jakarta.servlet.ServletRequest#getLocale() + * @see #addPreferredLocale(Locale) + * @see #setPreferredLocales(List) + */ + @Override + public Locale getLocale() { + return this.locales.getFirst(); + } + + /** + * Return an {@linkplain Enumeration enumeration} of the preferred + * {@linkplain Locale locales} configured in this mock request. + *

If no locales have been explicitly configured, the default, + * preferred {@link Locale} for the server mocked by this + * request is {@link Locale#ENGLISH}. + *

In contrast to the Servlet specification, this mock implementation + * does not take into consideration any locales + * specified via the {@code Accept-Language} header. + * @see jakarta.servlet.ServletRequest#getLocales() + * @see #addPreferredLocale(Locale) + * @see #setPreferredLocales(List) + */ + @Override + public Enumeration getLocales() { + return Collections.enumeration(this.locales); + } + + /** + * Set the boolean {@code secure} flag indicating whether the mock request + * was made using a secure channel, such as HTTPS. + * @see #isSecure() + * @see #getScheme() + * @see #setScheme(String) + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + /** + * Return {@code true} if the {@link #setSecure secure} flag has been set + * to {@code true} or if the {@link #getScheme scheme} is {@code https}. + * @see jakarta.servlet.ServletRequest#isSecure() + */ + @Override + public boolean isSecure() { + return (this.secure || HTTPS.equalsIgnoreCase(this.scheme)); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + return new MockRequestDispatcher(path); + } + + @Override + @Deprecated + public String getRealPath(String path) { + return this.servletContext.getRealPath(path); + } + + public void setRemotePort(int remotePort) { + this.remotePort = remotePort; + } + + @Override + public int getRemotePort() { + return this.remotePort; + } + + public void setLocalName(String localName) { + this.localName = localName; + } + + @Override + public String getLocalName() { + return this.localName; + } + + public void setLocalAddr(String localAddr) { + this.localAddr = localAddr; + } + + @Override + public String getLocalAddr() { + return this.localAddr; + } + + public void setLocalPort(int localPort) { + this.localPort = localPort; + } + + @Override + public int getLocalPort() { + return this.localPort; + } + + @Override + public AsyncContext startAsync() { + return startAsync(this, null); + } + + @Override + public AsyncContext startAsync(ServletRequest request, @Nullable ServletResponse response) { + Assert.state(this.asyncSupported, "Async not supported"); + this.asyncStarted = true; + this.asyncContext = + new MockAsyncContext(request, response); + return this.asyncContext; + } + + public void setAsyncStarted(boolean asyncStarted) { + this.asyncStarted = asyncStarted; + } + + @Override + public boolean isAsyncStarted() { + return this.asyncStarted; + } + + public void setAsyncSupported(boolean asyncSupported) { + this.asyncSupported = asyncSupported; + } + + @Override + public boolean isAsyncSupported() { + return this.asyncSupported; + } + + public void setAsyncContext(@Nullable MockAsyncContext asyncContext) { + this.asyncContext = asyncContext; + } + + @Override + @Nullable + public AsyncContext getAsyncContext() { + return this.asyncContext; + } + + public void setDispatcherType(DispatcherType dispatcherType) { + this.dispatcherType = dispatcherType; + } + + @Override + public DispatcherType getDispatcherType() { + return this.dispatcherType; + } + + + // --------------------------------------------------------------------- + // HttpServletRequest interface + // --------------------------------------------------------------------- + + public void setAuthType(@Nullable String authType) { + this.authType = authType; + } + + @Override + @Nullable + public String getAuthType() { + return this.authType; + } + + public void setCookies(@Nullable Cookie... cookies) { + this.cookies = (ObjectUtils.isEmpty(cookies) ? null : cookies); + if (this.cookies == null) { + removeHeader(HttpHeaders.COOKIE); + } + else { + doAddHeaderValue(HttpHeaders.COOKIE, encodeCookies(this.cookies), true); + } + } + + private static String encodeCookies(Cookie... cookies) { + return Arrays.stream(cookies) + .map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue())) + .collect(Collectors.joining("; ")); + } + + @Override + @Nullable + public Cookie[] getCookies() { + return this.cookies; + } + + /** + * Add an HTTP header entry for the given name. + *

While this method can take any {@code Object} as a parameter, + * it is recommended to use the following types: + *

    + *
  • String or any Object to be converted using {@code toString()}; see {@link #getHeader}.
  • + *
  • String, Number, or Date for date headers; see {@link #getDateHeader}.
  • + *
  • String or Number for integer headers; see {@link #getIntHeader}.
  • + *
  • {@code String[]} or {@code Collection} for multiple values; see {@link #getHeaders}.
  • + *
+ * @see #getHeaderNames + * @see #getHeaders + * @see #getHeader + * @see #getDateHeader + */ + public void addHeader(String name, Object value) { + if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) && + !this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) { + setContentType(value.toString()); + } + else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name) && + !this.headers.containsKey(HttpHeaders.ACCEPT_LANGUAGE)) { + try { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.ACCEPT_LANGUAGE, value.toString()); + List locales = headers.getAcceptLanguageAsLocales(); + this.locales.clear(); + this.locales.addAll(locales); + if (this.locales.isEmpty()) { + this.locales.add(Locale.ENGLISH); + } + } + catch (IllegalArgumentException ex) { + // Invalid Accept-Language format -> just store plain header + } + doAddHeaderValue(name, value, true); + } + else { + doAddHeaderValue(name, value, false); + } + } + + private void doAddHeaderValue(String name, @Nullable Object value, boolean replace) { + HeaderValueHolder header = this.headers.get(name); + Assert.notNull(value, "Header value must not be null"); + if (header == null || replace) { + header = new HeaderValueHolder(); + this.headers.put(name, header); + } + if (value instanceof Collection) { + header.addValues((Collection) value); + } + else if (value.getClass().isArray()) { + header.addValueArray(value); + } + else { + header.addValue(value); + } + } + + /** + * Remove already registered entries for the specified HTTP header, if any. + * @since 4.3.20 + */ + public void removeHeader(String name) { + Assert.notNull(name, "Header name must not be null"); + this.headers.remove(name); + } + + /** + * Return the long timestamp for the date header with the given {@code name}. + *

If the internal value representation is a String, this method will try + * to parse it as a date using the supported date formats: + *

    + *
  • "EEE, dd MMM yyyy HH:mm:ss zzz"
  • + *
  • "EEE, dd-MMM-yy HH:mm:ss zzz"
  • + *
  • "EEE MMM dd HH:mm:ss yyyy"
  • + *
+ * @param name the header name + * @see Section 7.1.1.1 of RFC 7231 + */ + @Override + public long getDateHeader(String name) { + HeaderValueHolder header = this.headers.get(name); + Object value = (header != null ? header.getValue() : null); + if (value instanceof Date) { + return ((Date) value).getTime(); + } + else if (value instanceof Number) { + return ((Number) value).longValue(); + } + else if (value instanceof String) { + return parseDateHeader(name, (String) value); + } + else if (value != null) { + throw new IllegalArgumentException( + "Value for header '" + name + "' is not a Date, Number, or String: " + value); + } + else { + return -1L; + } + } + + private long parseDateHeader(String name, String value) { + for (String dateFormat : DATE_FORMATS) { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US); + simpleDateFormat.setTimeZone(GMT); + try { + return simpleDateFormat.parse(value).getTime(); + } + catch (ParseException ex) { + // ignore + } + } + throw new IllegalArgumentException("Cannot parse date value '" + value + "' for '" + name + "' header"); + } + + @Override + @Nullable + public String getHeader(String name) { + HeaderValueHolder header = this.headers.get(name); + return (header != null ? header.getStringValue() : null); + } + + @Override + public Enumeration getHeaders(String name) { + HeaderValueHolder header = this.headers.get(name); + return Collections.enumeration(header != null ? header.getStringValues() : new LinkedList<>()); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(this.headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + HeaderValueHolder header = this.headers.get(name); + Object value = (header != null ? header.getValue() : null); + if (value instanceof Number) { + return ((Number) value).intValue(); + } + else if (value instanceof String) { + return Integer.parseInt((String) value); + } + else if (value != null) { + throw new NumberFormatException("Value for header '" + name + "' is not a Number: " + value); + } + else { + return -1; + } + } + + public void setMethod(@Nullable String method) { + this.method = method; + } + + @Override + @Nullable + public String getMethod() { + return this.method; + } + + public void setPathInfo(@Nullable String pathInfo) { + this.pathInfo = pathInfo; + } + + @Override + @Nullable + public String getPathInfo() { + return this.pathInfo; + } + + @Override + @Nullable + public String getPathTranslated() { + return (this.pathInfo != null ? getRealPath(this.pathInfo) : null); + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Override + public String getContextPath() { + return this.contextPath; + } + + public void setQueryString(@Nullable String queryString) { + this.queryString = queryString; + } + + @Override + @Nullable + public String getQueryString() { + return this.queryString; + } + + public void setRemoteUser(@Nullable String remoteUser) { + this.remoteUser = remoteUser; + } + + @Override + @Nullable + public String getRemoteUser() { + return this.remoteUser; + } + + public void addUserRole(String role) { + this.userRoles.add(role); + } + + @Override + public boolean isUserInRole(String role) { + return (this.userRoles.contains(role) || (this.servletContext instanceof MockServletContext && + ((MockServletContext) this.servletContext).getDeclaredRoles().contains(role))); + } + + public void setUserPrincipal(@Nullable Principal userPrincipal) { + this.userPrincipal = userPrincipal; + } + + @Override + @Nullable + public Principal getUserPrincipal() { + return this.userPrincipal; + } + + public void setRequestedSessionId(@Nullable String requestedSessionId) { + this.requestedSessionId = requestedSessionId; + } + + @Override + @Nullable + public String getRequestedSessionId() { + return this.requestedSessionId; + } + + public void setRequestURI(@Nullable String requestURI) { + this.requestURI = requestURI; + } + + @Override + @Nullable + public String getRequestURI() { + return this.requestURI; + } + + @Override + public StringBuffer getRequestURL() { + String scheme = getScheme(); + String server = getServerName(); + int port = getServerPort(); + String uri = getRequestURI(); + + StringBuffer url = new StringBuffer(scheme).append("://").append(server); + if (port > 0 && ((HTTP.equalsIgnoreCase(scheme) && port != 80) || + (HTTPS.equalsIgnoreCase(scheme) && port != 443))) { + url.append(':').append(port); + } + if (StringUtils.hasText(uri)) { + url.append(uri); + } + return url; + } + + public void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + @Override + public String getServletPath() { + return this.servletPath; + } + + public void setSession(HttpSession session) { + this.session = session; + if (session instanceof MockHttpSession) { + MockHttpSession mockSession = ((MockHttpSession) session); + mockSession.access(); + } + } + + @Override + @Nullable + public HttpSession getSession(boolean create) { + checkActive(); + // Reset session if invalidated. + if (this.session instanceof MockHttpSession && ((MockHttpSession) this.session).isInvalid()) { + this.session = null; + } + // Create new session if necessary. + if (this.session == null && create) { + this.session = new MockHttpSession(this.servletContext); + } + return this.session; + } + + @Override + @Nullable + public HttpSession getSession() { + return getSession(true); + } + + /** + * The implementation of this (Servlet 3.1+) method calls + * {@link MockHttpSession#changeSessionId()} if the session is a mock session. + * Otherwise it simply returns the current session id. + * @since 4.0.3 + */ + @Override + public String changeSessionId() { + Assert.isTrue(this.session != null, "The request does not have a session"); + if (this.session instanceof MockHttpSession) { + return ((MockHttpSession) this.session).changeSessionId(); + } + return this.session.getId(); + } + + public void setRequestedSessionIdValid(boolean requestedSessionIdValid) { + this.requestedSessionIdValid = requestedSessionIdValid; + } + + @Override + public boolean isRequestedSessionIdValid() { + return this.requestedSessionIdValid; + } + + public void setRequestedSessionIdFromCookie(boolean requestedSessionIdFromCookie) { + this.requestedSessionIdFromCookie = requestedSessionIdFromCookie; + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + return this.requestedSessionIdFromCookie; + } + + public void setRequestedSessionIdFromURL(boolean requestedSessionIdFromURL) { + this.requestedSessionIdFromURL = requestedSessionIdFromURL; + } + + @Override + public boolean isRequestedSessionIdFromURL() { + return this.requestedSessionIdFromURL; + } + + @Override + @Deprecated + public boolean isRequestedSessionIdFromUrl() { + return isRequestedSessionIdFromURL(); + } + + @Override + public boolean authenticate(HttpServletResponse response) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public void logout() throws ServletException { + this.userPrincipal = null; + this.remoteUser = null; + this.authType = null; + } + + public void addPart(Part part) { + this.parts.add(part.getName(), part); + } + + @Override + @Nullable + public Part getPart(String name) throws IOException, ServletException { + return this.parts.getFirst(name); + } + + @Override + public Collection getParts() throws IOException, ServletException { + List result = new LinkedList<>(); + for (List list : this.parts.values()) { + result.addAll(list); + } + return result; + } + + @Override + public T upgrade(Class handlerClass) throws IOException, ServletException { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockHttpServletResponse.java b/src/spec/java/org/springframework/mock/web/MockHttpServletResponse.java new file mode 100644 index 00000000..6f6a6512 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -0,0 +1,875 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; +import org.springframework.web.util.WebUtils; + +import java.io.*; +import java.nio.charset.Charset; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * Mock implementation of the {@link jakarta.servlet.http.HttpServletResponse} interface. + * + *

As of Spring 5.0, this set of mocks is designed on a Servlet 4.0 baseline. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Brian Clozel + * @author Vedran Pavic + * @author Sebastien Deleuze + * @author Sam Brannen + * @since 1.0.2 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockHttpServletResponse implements HttpServletResponse { + + private static final String CHARSET_PREFIX = "charset="; + + private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static final TimeZone GMT = TimeZone.getTimeZone("GMT"); + + + //--------------------------------------------------------------------- + // ServletResponse properties + //--------------------------------------------------------------------- + + private boolean outputStreamAccessAllowed = true; + + private boolean writerAccessAllowed = true; + + private String defaultCharacterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING; + + private String characterEncoding = this.defaultCharacterEncoding; + + /** + * {@code true} if the character encoding has been explicitly set through + * {@link HttpServletResponse} methods or through a {@code charset} parameter + * on the {@code Content-Type}. + */ + private boolean characterEncodingSet = false; + + private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024); + + private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content); + + @Nullable + private PrintWriter writer; + + private long contentLength = 0; + + @Nullable + private String contentType; + + private int bufferSize = 4096; + + private boolean committed; + + private Locale locale = Locale.getDefault(); + + + //--------------------------------------------------------------------- + // HttpServletResponse properties + //--------------------------------------------------------------------- + + private final List cookies = new ArrayList<>(); + + private final Map headers = new LinkedCaseInsensitiveMap<>(); + + private int status = HttpServletResponse.SC_OK; + + @Nullable + private String errorMessage; + + + //--------------------------------------------------------------------- + // Properties for MockRequestDispatcher + //--------------------------------------------------------------------- + + @Nullable + private String forwardedUrl; + + private final List includedUrls = new ArrayList<>(); + + + //--------------------------------------------------------------------- + // ServletResponse interface + //--------------------------------------------------------------------- + + /** + * Set whether {@link #getOutputStream()} access is allowed. + *

Default is {@code true}. + */ + public void setOutputStreamAccessAllowed(boolean outputStreamAccessAllowed) { + this.outputStreamAccessAllowed = outputStreamAccessAllowed; + } + + /** + * Return whether {@link #getOutputStream()} access is allowed. + */ + public boolean isOutputStreamAccessAllowed() { + return this.outputStreamAccessAllowed; + } + + /** + * Set whether {@link #getWriter()} access is allowed. + *

Default is {@code true}. + */ + public void setWriterAccessAllowed(boolean writerAccessAllowed) { + this.writerAccessAllowed = writerAccessAllowed; + } + + /** + * Return whether {@link #getOutputStream()} access is allowed. + */ + public boolean isWriterAccessAllowed() { + return this.writerAccessAllowed; + } + + /** + * Set the default character encoding for the response. + *

If this method is not invoked, {@code ISO-8859-1} will be used as the + * default character encoding. + *

If the {@linkplain #getCharacterEncoding() character encoding} for the + * response has not already been explicitly set via {@link #setCharacterEncoding(String)} + * or {@link #setContentType(String)}, the character encoding for the response + * will be set to the supplied default character encoding. + * @param characterEncoding the default character encoding + * @since 5.3.10 + * @see #setCharacterEncoding(String) + * @see #setContentType(String) + */ + public void setDefaultCharacterEncoding(String characterEncoding) { + Assert.notNull(characterEncoding, "'characterEncoding' must not be null"); + this.defaultCharacterEncoding = characterEncoding; + if (!this.characterEncodingSet) { + this.characterEncoding = characterEncoding; + } + } + + /** + * Determine whether the character encoding has been explicitly set through + * {@link HttpServletResponse} methods or through a {@code charset} parameter + * on the {@code Content-Type}. + *

If {@code false}, {@link #getCharacterEncoding()} will return the + * {@linkplain #setDefaultCharacterEncoding(String) default character encoding}. + */ + public boolean isCharset() { + return this.characterEncodingSet; + } + + @Override + public void setCharacterEncoding(String characterEncoding) { + setExplicitCharacterEncoding(characterEncoding); + updateContentTypePropertyAndHeader(); + } + + private void setExplicitCharacterEncoding(String characterEncoding) { + Assert.notNull(characterEncoding, "'characterEncoding' must not be null"); + this.characterEncoding = characterEncoding; + this.characterEncodingSet = true; + } + + private void updateContentTypePropertyAndHeader() { + if (this.contentType != null) { + String value = this.contentType; + if (this.characterEncodingSet && !value.toLowerCase().contains(CHARSET_PREFIX)) { + value += ';' + CHARSET_PREFIX + getCharacterEncoding(); + this.contentType = value; + } + doAddHeaderValue(HttpHeaders.CONTENT_TYPE, value, true); + } + } + + @Override + public String getCharacterEncoding() { + return this.characterEncoding; + } + + @Override + public ServletOutputStream getOutputStream() { + Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed"); + return this.outputStream; + } + + @Override + public PrintWriter getWriter() throws UnsupportedEncodingException { + Assert.state(this.writerAccessAllowed, "Writer access not allowed"); + if (this.writer == null) { + Writer targetWriter = new OutputStreamWriter(this.content, getCharacterEncoding()); + this.writer = new ResponsePrintWriter(targetWriter); + } + return this.writer; + } + + public byte[] getContentAsByteArray() { + return this.content.toByteArray(); + } + + /** + * Get the content of the response body as a {@code String}, using the charset + * specified for the response by the application, either through + * {@link HttpServletResponse} methods or through a charset parameter on the + * {@code Content-Type}. If no charset has been explicitly defined, the + * {@linkplain #setDefaultCharacterEncoding(String) default character encoding} + * will be used. + * @return the content as a {@code String} + * @throws UnsupportedEncodingException if the character encoding is not supported + * @see #getContentAsString(Charset) + * @see #setCharacterEncoding(String) + * @see #setContentType(String) + */ + public String getContentAsString() throws UnsupportedEncodingException { + return this.content.toString(getCharacterEncoding()); + } + + /** + * Get the content of the response body as a {@code String}, using the provided + * {@code fallbackCharset} if no charset has been explicitly defined and otherwise + * using the charset specified for the response by the application, either + * through {@link HttpServletResponse} methods or through a charset parameter on the + * {@code Content-Type}. + * @return the content as a {@code String} + * @throws UnsupportedEncodingException if the character encoding is not supported + * @since 5.2 + * @see #getContentAsString() + * @see #setCharacterEncoding(String) + * @see #setContentType(String) + */ + public String getContentAsString(Charset fallbackCharset) throws UnsupportedEncodingException { + String charsetName = (this.characterEncodingSet ? getCharacterEncoding() : fallbackCharset.name()); + return this.content.toString(charsetName); + } + + @Override + public void setContentLength(int contentLength) { + this.contentLength = contentLength; + doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + } + + public int getContentLength() { + return (int) this.contentLength; + } + + @Override + public void setContentLengthLong(long contentLength) { + this.contentLength = contentLength; + doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + } + + public long getContentLengthLong() { + return this.contentLength; + } + + @Override + public void setContentType(@Nullable String contentType) { + this.contentType = contentType; + if (contentType != null) { + try { + MediaType mediaType = MediaType.parseMediaType(contentType); + if (mediaType.getCharset() != null) { + setExplicitCharacterEncoding(mediaType.getCharset().name()); + } + } + catch (Exception ex) { + // Try to get charset value anyway + int charsetIndex = contentType.toLowerCase().indexOf(CHARSET_PREFIX); + if (charsetIndex != -1) { + setExplicitCharacterEncoding(contentType.substring(charsetIndex + CHARSET_PREFIX.length())); + } + } + updateContentTypePropertyAndHeader(); + } + } + + @Override + @Nullable + public String getContentType() { + return this.contentType; + } + + @Override + public void setBufferSize(int bufferSize) { + this.bufferSize = bufferSize; + } + + @Override + public int getBufferSize() { + return this.bufferSize; + } + + @Override + public void flushBuffer() { + setCommitted(true); + } + + @Override + public void resetBuffer() { + Assert.state(!isCommitted(), "Cannot reset buffer - response is already committed"); + this.content.reset(); + } + + private void setCommittedIfBufferSizeExceeded() { + int bufSize = getBufferSize(); + if (bufSize > 0 && this.content.size() > bufSize) { + setCommitted(true); + } + } + + public void setCommitted(boolean committed) { + this.committed = committed; + } + + @Override + public boolean isCommitted() { + return this.committed; + } + + @Override + public void reset() { + resetBuffer(); + this.characterEncoding = this.defaultCharacterEncoding; + this.characterEncodingSet = false; + this.contentLength = 0; + this.contentType = null; + this.locale = Locale.getDefault(); + this.cookies.clear(); + this.headers.clear(); + this.status = HttpServletResponse.SC_OK; + this.errorMessage = null; + } + + @Override + public void setLocale(@Nullable Locale locale) { + // Although the Javadoc for jakarta.servlet.ServletResponse.setLocale(Locale) does not + // state how a null value for the supplied Locale should be handled, both Tomcat and + // Jetty simply ignore a null value. So we do the same here. + if (locale == null) { + return; + } + this.locale = locale; + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, locale.toLanguageTag(), true); + } + + @Override + public Locale getLocale() { + return this.locale; + } + + + //--------------------------------------------------------------------- + // HttpServletResponse interface + //--------------------------------------------------------------------- + + @Override + public void addCookie(Cookie cookie) { + Assert.notNull(cookie, "Cookie must not be null"); + this.cookies.add(cookie); + doAddHeaderValue(HttpHeaders.SET_COOKIE, getCookieHeader(cookie), false); + } + + private String getCookieHeader(Cookie cookie) { + StringBuilder buf = new StringBuilder(); + buf.append(cookie.getName()).append('=').append(cookie.getValue() == null ? "" : cookie.getValue()); + if (StringUtils.hasText(cookie.getPath())) { + buf.append("; Path=").append(cookie.getPath()); + } + if (StringUtils.hasText(cookie.getDomain())) { + buf.append("; Domain=").append(cookie.getDomain()); + } + int maxAge = cookie.getMaxAge(); + ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null); + if (maxAge >= 0) { + buf.append("; Max-Age=").append(maxAge); + buf.append("; Expires="); + if (expires != null) { + buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); + } + else { + HttpHeaders headers = new HttpHeaders(); + headers.setExpires(maxAge > 0 ? System.currentTimeMillis() + 1000L * maxAge : 0); + buf.append(headers.getFirst(HttpHeaders.EXPIRES)); + } + } + else if (expires != null) { + buf.append("; Expires="); + buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME)); + } + + if (cookie.getSecure()) { + buf.append("; Secure"); + } + if (cookie.isHttpOnly()) { + buf.append("; HttpOnly"); + } + if (cookie instanceof MockCookie) { + MockCookie mockCookie = (MockCookie) cookie; + if (StringUtils.hasText(mockCookie.getSameSite())) { + buf.append("; SameSite=").append(mockCookie.getSameSite()); + } + } + if (StringUtils.hasText(cookie.getComment())) { + buf.append("; Comment=").append(cookie.getComment()); + } + return buf.toString(); + } + + public Cookie[] getCookies() { + return this.cookies.toArray(new Cookie[0]); + } + + @Nullable + public Cookie getCookie(String name) { + Assert.notNull(name, "Cookie name must not be null"); + for (Cookie cookie : this.cookies) { + if (name.equals(cookie.getName())) { + return cookie; + } + } + return null; + } + + @Override + public boolean containsHeader(String name) { + return this.headers.containsKey(name); + } + + /** + * Return the names of all specified headers as a Set of Strings. + *

As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. + * @return the {@code Set} of header name {@code Strings}, or an empty {@code Set} if none + */ + @Override + public Collection getHeaderNames() { + return this.headers.keySet(); + } + + /** + * Return the primary value for the given header as a String, if any. + * Will return the first value in case of multiple values. + *

As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. + * As of Spring 3.1, it returns a stringified value for Servlet 3.0 compatibility. + * Consider using {@link #getHeaderValue(String)} for raw Object access. + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + @Override + @Nullable + public String getHeader(String name) { + HeaderValueHolder header = this.headers.get(name); + return (header != null ? header.getStringValue() : null); + } + + /** + * Return all values for the given header as a List of Strings. + *

As of Servlet 3.0, this method is also defined in {@link HttpServletResponse}. + * As of Spring 3.1, it returns a List of stringified values for Servlet 3.0 compatibility. + * Consider using {@link #getHeaderValues(String)} for raw Object access. + * @param name the name of the header + * @return the associated header values, or an empty List if none + */ + @Override + public List getHeaders(String name) { + HeaderValueHolder header = this.headers.get(name); + if (header != null) { + return header.getStringValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * Return the primary value for the given header, if any. + *

Will return the first value in case of multiple values. + * @param name the name of the header + * @return the associated header value, or {@code null} if none + */ + @Nullable + public Object getHeaderValue(String name) { + HeaderValueHolder header = this.headers.get(name); + return (header != null ? header.getValue() : null); + } + + /** + * Return all values for the given header as a List of value objects. + * @param name the name of the header + * @return the associated header values, or an empty List if none + */ + public List getHeaderValues(String name) { + HeaderValueHolder header = this.headers.get(name); + if (header != null) { + return header.getValues(); + } + else { + return Collections.emptyList(); + } + } + + /** + * The default implementation returns the given URL String as-is. + *

Can be overridden in subclasses, appending a session id or the like. + */ + @Override + public String encodeURL(String url) { + return url; + } + + /** + * The default implementation delegates to {@link #encodeURL}, + * returning the given URL String as-is. + *

Can be overridden in subclasses, appending a session id or the like + * in a redirect-specific fashion. For general URL encoding rules, + * override the common {@link #encodeURL} method instead, applying + * to redirect URLs as well as to general URLs. + */ + @Override + public String encodeRedirectURL(String url) { + return encodeURL(url); + } + + @Override + @Deprecated + public String encodeUrl(String url) { + return encodeURL(url); + } + + @Override + @Deprecated + public String encodeRedirectUrl(String url) { + return encodeRedirectURL(url); + } + + @Override + public void sendError(int status, String errorMessage) throws IOException { + Assert.state(!isCommitted(), "Cannot set error status - response is already committed"); + this.status = status; + this.errorMessage = errorMessage; + setCommitted(true); + } + + @Override + public void sendError(int status) throws IOException { + Assert.state(!isCommitted(), "Cannot set error status - response is already committed"); + this.status = status; + setCommitted(true); + } + + @Override + public void sendRedirect(String url) throws IOException { + Assert.state(!isCommitted(), "Cannot send redirect - response is already committed"); + Assert.notNull(url, "Redirect URL must not be null"); + setHeader(HttpHeaders.LOCATION, url); + setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + setCommitted(true); + } + + @Nullable + public String getRedirectedUrl() { + return getHeader(HttpHeaders.LOCATION); + } + + @Override + public void setDateHeader(String name, long value) { + setHeaderValue(name, formatDate(value)); + } + + @Override + public void addDateHeader(String name, long value) { + addHeaderValue(name, formatDate(value)); + } + + public long getDateHeader(String name) { + String headerValue = getHeader(name); + if (headerValue == null) { + return -1; + } + try { + return newDateFormat().parse(getHeader(name)).getTime(); + } + catch (ParseException ex) { + throw new IllegalArgumentException( + "Value for header '" + name + "' is not a valid Date: " + headerValue); + } + } + + private String formatDate(long date) { + return newDateFormat().format(new Date(date)); + } + + private DateFormat newDateFormat() { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); + dateFormat.setTimeZone(GMT); + return dateFormat; + } + + @Override + public void setHeader(String name, @Nullable String value) { + setHeaderValue(name, value); + } + + @Override + public void addHeader(String name, @Nullable String value) { + addHeaderValue(name, value); + } + + @Override + public void setIntHeader(String name, int value) { + setHeaderValue(name, value); + } + + @Override + public void addIntHeader(String name, int value) { + addHeaderValue(name, value); + } + + private void setHeaderValue(String name, @Nullable Object value) { + if (value == null) { + return; + } + boolean replaceHeader = true; + if (setSpecialHeader(name, value, replaceHeader)) { + return; + } + doAddHeaderValue(name, value, replaceHeader); + } + + private void addHeaderValue(String name, @Nullable Object value) { + if (value == null) { + return; + } + boolean replaceHeader = false; + if (setSpecialHeader(name, value, replaceHeader)) { + return; + } + doAddHeaderValue(name, value, replaceHeader); + } + + private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) { + if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + setContentType(value.toString()); + return true; + } + else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + setContentLength(value instanceof Number ? ((Number) value).intValue() : + Integer.parseInt(value.toString())); + return true; + } + else if (HttpHeaders.CONTENT_LANGUAGE.equalsIgnoreCase(name)) { + String contentLanguages = value.toString(); + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.CONTENT_LANGUAGE, contentLanguages); + Locale language = headers.getContentLanguage(); + setLocale(language != null ? language : Locale.getDefault()); + // Since setLocale() sets the Content-Language header to the given + // single Locale, we have to explicitly set the Content-Language header + // to the user-provided value. + doAddHeaderValue(HttpHeaders.CONTENT_LANGUAGE, contentLanguages, true); + return true; + } + else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) { + MockCookie cookie = MockCookie.parse(value.toString()); + if (replaceHeader) { + setCookie(cookie); + } + else { + addCookie(cookie); + } + return true; + } + else { + return false; + } + } + + private void doAddHeaderValue(String name, Object value, boolean replace) { + Assert.notNull(value, "Header value must not be null"); + HeaderValueHolder header = this.headers.computeIfAbsent(name, key -> new HeaderValueHolder()); + if (replace) { + header.setValue(value); + } + else { + header.addValue(value); + } + } + + /** + * Set the {@code Set-Cookie} header to the supplied {@link Cookie}, + * overwriting any previous cookies. + * @param cookie the {@code Cookie} to set + * @since 5.1.10 + * @see #addCookie(Cookie) + */ + private void setCookie(Cookie cookie) { + Assert.notNull(cookie, "Cookie must not be null"); + this.cookies.clear(); + this.cookies.add(cookie); + doAddHeaderValue(HttpHeaders.SET_COOKIE, getCookieHeader(cookie), true); + } + + @Override + public void setStatus(int status) { + if (!this.isCommitted()) { + this.status = status; + } + } + + @Override + @Deprecated + public void setStatus(int status, String errorMessage) { + if (!this.isCommitted()) { + this.status = status; + this.errorMessage = errorMessage; + } + } + + @Override + public int getStatus() { + return this.status; + } + + @Nullable + public String getErrorMessage() { + return this.errorMessage; + } + + + //--------------------------------------------------------------------- + // Methods for MockRequestDispatcher + //--------------------------------------------------------------------- + + public void setForwardedUrl(@Nullable String forwardedUrl) { + this.forwardedUrl = forwardedUrl; + } + + @Nullable + public String getForwardedUrl() { + return this.forwardedUrl; + } + + public void setIncludedUrl(@Nullable String includedUrl) { + this.includedUrls.clear(); + if (includedUrl != null) { + this.includedUrls.add(includedUrl); + } + } + + @Nullable + public String getIncludedUrl() { + int count = this.includedUrls.size(); + Assert.state(count <= 1, + () -> "More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls); + return (count == 1 ? this.includedUrls.get(0) : null); + } + + public void addIncludedUrl(String includedUrl) { + Assert.notNull(includedUrl, "Included URL must not be null"); + this.includedUrls.add(includedUrl); + } + + public List getIncludedUrls() { + return this.includedUrls; + } + + + /** + * Inner class that adapts the ServletOutputStream to mark the + * response as committed once the buffer size is exceeded. + */ + private class ResponseServletOutputStream extends DelegatingServletOutputStream { + + public ResponseServletOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + super.write(b); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + @Override + public void flush() throws IOException { + super.flush(); + setCommitted(true); + } + } + + + /** + * Inner class that adapts the PrintWriter to mark the + * response as committed once the buffer size is exceeded. + */ + private class ResponsePrintWriter extends PrintWriter { + + public ResponsePrintWriter(Writer out) { + super(out, true); + } + + @Override + public void write(char[] buf, int off, int len) { + super.write(buf, off, len); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + @Override + public void write(String s, int off, int len) { + super.write(s, off, len); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + @Override + public void write(int c) { + super.write(c); + super.flush(); + setCommittedIfBufferSizeExceeded(); + } + + @Override + public void flush() { + super.flush(); + setCommitted(true); + } + + @Override + public void close() { + super.flush(); + super.close(); + setCommitted(true); + } + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockHttpSession.java b/src/spec/java/org/springframework/mock/web/MockHttpSession.java new file mode 100644 index 00000000..344062d7 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockHttpSession.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpSession; +import jakarta.servlet.http.HttpSessionBindingEvent; +import jakarta.servlet.http.HttpSessionBindingListener; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.util.*; + +/** + * Mock implementation of the {@link jakarta.servlet.http.HttpSession} interface. + * + *

As of Spring 5.0, this set of mocks is designed on a Servlet 4.0 baseline. + * + * @author Juergen Hoeller + * @author Rod Johnson + * @author Mark Fisher + * @author Sam Brannen + * @author Vedran Pavic + * @since 1.0.2 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +@SuppressWarnings("deprecation") +public class MockHttpSession implements HttpSession { + + private static int nextId = 1; + + private String id; + + private final long creationTime = System.currentTimeMillis(); + + private int maxInactiveInterval; + + private long lastAccessedTime = System.currentTimeMillis(); + + private final ServletContext servletContext; + + private final Map attributes = new LinkedHashMap<>(); + + private boolean invalid = false; + + private boolean isNew = true; + + + /** + * Create a new MockHttpSession with a default {@link MockServletContext}. + * @see MockServletContext + */ + public MockHttpSession() { + this(null); + } + + /** + * Create a new MockHttpSession. + * @param servletContext the ServletContext that the session runs in + */ + public MockHttpSession(@Nullable ServletContext servletContext) { + this(servletContext, null); + } + + /** + * Create a new MockHttpSession. + * @param servletContext the ServletContext that the session runs in + * @param id a unique identifier for this session + */ + public MockHttpSession(@Nullable ServletContext servletContext, @Nullable String id) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.id = (id != null ? id : Integer.toString(nextId++)); + } + + + @Override + public long getCreationTime() { + assertIsValid(); + return this.creationTime; + } + + @Override + public String getId() { + return this.id; + } + + /** + * As of Servlet 3.1, the id of a session can be changed. + * @return the new session id + * @since 4.0.3 + */ + public String changeSessionId() { + this.id = Integer.toString(nextId++); + return this.id; + } + + public void access() { + this.lastAccessedTime = System.currentTimeMillis(); + this.isNew = false; + } + + @Override + public long getLastAccessedTime() { + assertIsValid(); + return this.lastAccessedTime; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + @Override + public void setMaxInactiveInterval(int interval) { + this.maxInactiveInterval = interval; + } + + @Override + public int getMaxInactiveInterval() { + return this.maxInactiveInterval; + } + + @Override + public jakarta.servlet.http.HttpSessionContext getSessionContext() { + throw new UnsupportedOperationException("getSessionContext"); + } + + @Override + public Object getAttribute(String name) { + assertIsValid(); + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + @Override + public Object getValue(String name) { + return getAttribute(name); + } + + @Override + public Enumeration getAttributeNames() { + assertIsValid(); + return Collections.enumeration(new LinkedHashSet<>(this.attributes.keySet())); + } + + @Override + public String[] getValueNames() { + assertIsValid(); + return StringUtils.toStringArray(this.attributes.keySet()); + } + + @Override + public void setAttribute(String name, @Nullable Object value) { + assertIsValid(); + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + Object oldValue = this.attributes.put(name, value); + if (value != oldValue) { + if (oldValue instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) oldValue).valueUnbound(new HttpSessionBindingEvent(this, name, oldValue)); + } + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + else { + removeAttribute(name); + } + } + + @Override + public void putValue(String name, Object value) { + setAttribute(name, value); + } + + @Override + public void removeAttribute(String name) { + assertIsValid(); + Assert.notNull(name, "Attribute name must not be null"); + Object value = this.attributes.remove(name); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + + @Override + public void removeValue(String name) { + removeAttribute(name); + } + + /** + * Clear all of this session's attributes. + */ + public void clearAttributes() { + for (Iterator> it = this.attributes.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + String name = entry.getKey(); + Object value = entry.getValue(); + it.remove(); + if (value instanceof HttpSessionBindingListener) { + ((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value)); + } + } + } + + /** + * Invalidates this session then unbinds any objects bound to it. + * @throws IllegalStateException if this method is called on an already invalidated session + */ + @Override + public void invalidate() { + assertIsValid(); + this.invalid = true; + clearAttributes(); + } + + public boolean isInvalid() { + return this.invalid; + } + + /** + * Convenience method for asserting that this session has not been + * {@linkplain #invalidate() invalidated}. + * @throws IllegalStateException if this session has been invalidated + */ + private void assertIsValid() { + Assert.state(!isInvalid(), "The session has already been invalidated"); + } + + public void setNew(boolean value) { + this.isNew = value; + } + + @Override + public boolean isNew() { + assertIsValid(); + return this.isNew; + } +} diff --git a/src/spec/java/org/springframework/mock/web/MockRequestDispatcher.java b/src/spec/java/org/springframework/mock/web/MockRequestDispatcher.java new file mode 100644 index 00000000..a6ec3fd9 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockRequestDispatcher.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletResponseWrapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; + +/** + * Mock implementation of the {@link jakarta.servlet.RequestDispatcher} interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.0.2 + * @see MockHttpServletRequest#getRequestDispatcher(String) + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockRequestDispatcher implements RequestDispatcher { + + private final Log logger = LogFactory.getLog(getClass()); + + private final String resource; + + + /** + * Create a new MockRequestDispatcher for the given resource. + * @param resource the server resource to dispatch to, located at a + * particular path or given by a particular name + */ + public MockRequestDispatcher(String resource) { + Assert.notNull(resource, "Resource must not be null"); + this.resource = resource; + } + + + @Override + public void forward(ServletRequest request, ServletResponse response) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + Assert.state(!response.isCommitted(), "Cannot perform forward - response is already committed"); + getMockHttpServletResponse(response).setForwardedUrl(this.resource); + if (logger.isDebugEnabled()) { + logger.debug("MockRequestDispatcher: forwarding to [" + this.resource + "]"); + } + } + + @Override + public void include(ServletRequest request, ServletResponse response) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(response, "Response must not be null"); + getMockHttpServletResponse(response).addIncludedUrl(this.resource); + if (logger.isDebugEnabled()) { + logger.debug("MockRequestDispatcher: including [" + this.resource + "]"); + } + } + + /** + * Obtain the underlying {@link MockHttpServletResponse}, unwrapping + * {@link HttpServletResponseWrapper} decorators if necessary. + */ + protected MockHttpServletResponse getMockHttpServletResponse(ServletResponse response) { + if (response instanceof MockHttpServletResponse) { + return (MockHttpServletResponse) response; + } + if (response instanceof HttpServletResponseWrapper) { + return getMockHttpServletResponse(((HttpServletResponseWrapper) response).getResponse()); + } + throw new IllegalArgumentException("MockRequestDispatcher requires MockHttpServletResponse"); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockServletConfig.java b/src/spec/java/org/springframework/mock/web/MockServletConfig.java new file mode 100644 index 00000000..b4685a5b --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockServletConfig.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Mock implementation of the {@link jakarta.servlet.ServletConfig} interface. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @since 1.0.2 + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockServletConfig implements ServletConfig { + + private final ServletContext servletContext; + + private final String servletName; + + private final Map initParameters = new LinkedHashMap<>(); + + + /** + * Create a new MockServletConfig with a default {@link MockServletContext}. + */ + public MockServletConfig() { + this(null, ""); + } + + /** + * Create a new MockServletConfig with a default {@link MockServletContext}. + * @param servletName the name of the servlet + */ + public MockServletConfig(String servletName) { + this(null, servletName); + } + + /** + * Create a new MockServletConfig. + * @param servletContext the ServletContext that the servlet runs in + */ + public MockServletConfig(@Nullable ServletContext servletContext) { + this(servletContext, ""); + } + + /** + * Create a new MockServletConfig. + * @param servletContext the ServletContext that the servlet runs in + * @param servletName the name of the servlet + */ + public MockServletConfig(@Nullable ServletContext servletContext, String servletName) { + this.servletContext = (servletContext != null ? servletContext : new MockServletContext()); + this.servletName = servletName; + } + + + @Override + public String getServletName() { + return this.servletName; + } + + @Override + public ServletContext getServletContext() { + return this.servletContext; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + @Override + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockServletContext.java b/src/spec/java/org/springframework/mock/web/MockServletContext.java new file mode 100644 index 00000000..c154c1f5 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockServletContext.java @@ -0,0 +1,728 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.*; +import jakarta.servlet.descriptor.JspConfigDescriptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; +import org.springframework.http.MediaTypeFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.*; +import org.springframework.web.util.WebUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.InvalidPathException; +import java.util.*; + +/** + * Mock implementation of the {@link jakarta.servlet.ServletContext} interface. + * + *

As of Spring 5.0, this set of mocks is designed on a Servlet 4.0 baseline. + * + *

Compatible with Servlet 3.1 but can be configured to expose a specific version + * through {@link #setMajorVersion}/{@link #setMinorVersion}; default is 3.1. + * Note that Servlet 3.1 support is limited: servlet, filter and listener + * registration methods are not supported; neither is JSP configuration. + * We generally do not recommend to unit test your ServletContainerInitializers and + * WebApplicationInitializers which is where those registration methods would be used. + * + *

For setting up a full {@code WebApplicationContext} in a test environment, you can + * use {@code AnnotationConfigWebApplicationContext}, {@code XmlWebApplicationContext}, + * or {@code GenericWebApplicationContext}, passing in a corresponding + * {@code MockServletContext} instance. Consider configuring your + * {@code MockServletContext} with a {@code FileSystemResourceLoader} in order to + * interpret resource paths as relative filesystem locations. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @since 1.0.2 + * @see #MockServletContext(ResourceLoader) + * @see org.springframework.web.context.support.AnnotationConfigWebApplicationContext + * @see org.springframework.web.context.support.XmlWebApplicationContext + * @see org.springframework.web.context.support.GenericWebApplicationContext + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockServletContext implements ServletContext { + + /** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish: {@value}. */ + private static final String COMMON_DEFAULT_SERVLET_NAME = "default"; + + private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; + + private static final Set DEFAULT_SESSION_TRACKING_MODES = new LinkedHashSet<>(4); + + static { + DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.COOKIE); + DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.URL); + DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.SSL); + } + + + private final Log logger = LogFactory.getLog(getClass()); + + private final ResourceLoader resourceLoader; + + private final String resourceBasePath; + + private String contextPath = ""; + + private final Map contexts = new HashMap<>(); + + private int majorVersion = 3; + + private int minorVersion = 1; + + private int effectiveMajorVersion = 3; + + private int effectiveMinorVersion = 1; + + private final Map namedRequestDispatchers = new HashMap<>(); + + private String defaultServletName = COMMON_DEFAULT_SERVLET_NAME; + + private final Map initParameters = new LinkedHashMap<>(); + + private final Map attributes = new LinkedHashMap<>(); + + private String servletContextName = "MockServletContext"; + + private final Set declaredRoles = new LinkedHashSet<>(); + + @Nullable + private Set sessionTrackingModes; + + private final SessionCookieConfig sessionCookieConfig = new MockSessionCookieConfig(); + + private int sessionTimeout; + + @Nullable + private String requestCharacterEncoding; + + @Nullable + private String responseCharacterEncoding; + + private final Map mimeTypes = new LinkedHashMap<>(); + + + /** + * Create a new {@code MockServletContext}, using no base path and a + * {@link DefaultResourceLoader} (i.e. the classpath root as WAR root). + * @see DefaultResourceLoader + */ + public MockServletContext() { + this("", null); + } + + /** + * Create a new {@code MockServletContext}, using a {@link DefaultResourceLoader}. + * @param resourceBasePath the root directory of the WAR (should not end with a slash) + * @see DefaultResourceLoader + */ + public MockServletContext(String resourceBasePath) { + this(resourceBasePath, null); + } + + /** + * Create a new {@code MockServletContext}, using the specified {@link ResourceLoader} + * and no base path. + * @param resourceLoader the ResourceLoader to use (or null for the default) + */ + public MockServletContext(@Nullable ResourceLoader resourceLoader) { + this("", resourceLoader); + } + + /** + * Create a new {@code MockServletContext} using the supplied resource base + * path and resource loader. + *

Registers a {@link MockRequestDispatcher} for the Servlet named + * {@literal 'default'}. + * @param resourceBasePath the root directory of the WAR (should not end with a slash) + * @param resourceLoader the ResourceLoader to use (or null for the default) + * @see #registerNamedDispatcher + */ + public MockServletContext(String resourceBasePath, @Nullable ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + this.resourceBasePath = resourceBasePath; + + // Use JVM temp dir as ServletContext temp dir. + String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY); + if (tempDir != null) { + this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir)); + } + + registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); + } + + /** + * Build a full resource location for the given path, prepending the resource + * base path of this {@code MockServletContext}. + * @param path the path as specified + * @return the full resource path + */ + protected String getResourceLocation(String path) { + if (!path.startsWith("/")) { + path = "/" + path; + } + return this.resourceBasePath + path; + } + + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + @Override + public String getContextPath() { + return this.contextPath; + } + + public void registerContext(String contextPath, ServletContext context) { + this.contexts.put(contextPath, context); + } + + @Override + public ServletContext getContext(String contextPath) { + if (this.contextPath.equals(contextPath)) { + return this; + } + return this.contexts.get(contextPath); + } + + public void setMajorVersion(int majorVersion) { + this.majorVersion = majorVersion; + } + + @Override + public int getMajorVersion() { + return this.majorVersion; + } + + public void setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + } + + @Override + public int getMinorVersion() { + return this.minorVersion; + } + + public void setEffectiveMajorVersion(int effectiveMajorVersion) { + this.effectiveMajorVersion = effectiveMajorVersion; + } + + @Override + public int getEffectiveMajorVersion() { + return this.effectiveMajorVersion; + } + + public void setEffectiveMinorVersion(int effectiveMinorVersion) { + this.effectiveMinorVersion = effectiveMinorVersion; + } + + @Override + public int getEffectiveMinorVersion() { + return this.effectiveMinorVersion; + } + + @Override + @Nullable + public String getMimeType(String filePath) { + String extension = StringUtils.getFilenameExtension(filePath); + if (this.mimeTypes.containsKey(extension)) { + return this.mimeTypes.get(extension).toString(); + } + else { + return MediaTypeFactory.getMediaType(filePath). + map(MimeType::toString) + .orElse(null); + } + } + + /** + * Adds a mime type mapping for use by {@link #getMimeType(String)}. + * @param fileExtension a file extension, such as {@code txt}, {@code gif} + * @param mimeType the mime type + */ + public void addMimeType(String fileExtension, MediaType mimeType) { + Assert.notNull(fileExtension, "'fileExtension' must not be null"); + this.mimeTypes.put(fileExtension, mimeType); + } + + @Override + @Nullable + public Set getResourcePaths(String path) { + String actualPath = (path.endsWith("/") ? path : path + "/"); + String resourceLocation = getResourceLocation(actualPath); + Resource resource = null; + try { + resource = this.resourceLoader.getResource(resourceLocation); + File file = resource.getFile(); + String[] fileList = file.list(); + if (ObjectUtils.isEmpty(fileList)) { + return null; + } + Set resourcePaths = new LinkedHashSet<>(fileList.length); + for (String fileEntry : fileList) { + String resultPath = actualPath + fileEntry; + if (resource.createRelative(fileEntry).getFile().isDirectory()) { + resultPath += "/"; + } + resourcePaths.add(resultPath); + } + return resourcePaths; + } + catch (InvalidPathException | IOException ex ) { + if (logger.isDebugEnabled()) { + logger.debug("Could not get resource paths for " + + (resource != null ? resource : resourceLocation), ex); + } + return null; + } + } + + @Override + @Nullable + public URL getResource(String path) throws MalformedURLException { + String resourceLocation = getResourceLocation(path); + Resource resource = null; + try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } + return resource.getURL(); + } + catch (MalformedURLException ex) { + throw ex; + } + catch (InvalidPathException | IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not get URL for resource " + + (resource != null ? resource : resourceLocation), ex); + } + return null; + } + } + + @Override + @Nullable + public InputStream getResourceAsStream(String path) { + String resourceLocation = getResourceLocation(path); + Resource resource = null; + try { + resource = this.resourceLoader.getResource(resourceLocation); + if (!resource.exists()) { + return null; + } + return resource.getInputStream(); + } + catch (InvalidPathException | IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not open InputStream for resource " + + (resource != null ? resource : resourceLocation), ex); + } + return null; + } + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + Assert.isTrue(path.startsWith("/"), + () -> "RequestDispatcher path [" + path + "] at ServletContext level must start with '/'"); + return new MockRequestDispatcher(path); + } + + @Override + public RequestDispatcher getNamedDispatcher(String path) { + return this.namedRequestDispatchers.get(path); + } + + /** + * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher}) + * that acts as a wrapper for the named Servlet. + * @param name the name of the wrapped Servlet + * @param requestDispatcher the dispatcher that wraps the named Servlet + * @see #getNamedDispatcher + * @see #unregisterNamedDispatcher + */ + public void registerNamedDispatcher(String name, RequestDispatcher requestDispatcher) { + Assert.notNull(name, "RequestDispatcher name must not be null"); + Assert.notNull(requestDispatcher, "RequestDispatcher must not be null"); + this.namedRequestDispatchers.put(name, requestDispatcher); + } + + /** + * Unregister the {@link RequestDispatcher} with the given name. + * @param name the name of the dispatcher to unregister + * @see #getNamedDispatcher + * @see #registerNamedDispatcher + */ + public void unregisterNamedDispatcher(String name) { + Assert.notNull(name, "RequestDispatcher name must not be null"); + this.namedRequestDispatchers.remove(name); + } + + /** + * Get the name of the default {@code Servlet}. + *

Defaults to {@literal 'default'}. + * @see #setDefaultServletName + */ + public String getDefaultServletName() { + return this.defaultServletName; + } + + /** + * Set the name of the default {@code Servlet}. + *

Also {@link #unregisterNamedDispatcher unregisters} the current default + * {@link RequestDispatcher} and {@link #registerNamedDispatcher replaces} + * it with a {@link MockRequestDispatcher} for the provided + * {@code defaultServletName}. + * @param defaultServletName the name of the default {@code Servlet}; + * never {@code null} or empty + * @see #getDefaultServletName + */ + public void setDefaultServletName(String defaultServletName) { + Assert.hasText(defaultServletName, "defaultServletName must not be null or empty"); + unregisterNamedDispatcher(this.defaultServletName); + this.defaultServletName = defaultServletName; + registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); + } + + @Deprecated + @Override + @Nullable + public Servlet getServlet(String name) { + return null; + } + + @Override + @Deprecated + public Enumeration getServlets() { + return Collections.enumeration(Collections.emptySet()); + } + + @Override + @Deprecated + public Enumeration getServletNames() { + return Collections.enumeration(Collections.emptySet()); + } + + @Override + public void log(String message) { + logger.info(message); + } + + @Override + @Deprecated + public void log(Exception ex, String message) { + logger.info(message, ex); + } + + @Override + public void log(String message, Throwable ex) { + logger.info(message, ex); + } + + @Override + @Nullable + public String getRealPath(String path) { + String resourceLocation = getResourceLocation(path); + Resource resource = null; + try { + resource = this.resourceLoader.getResource(resourceLocation); + return resource.getFile().getAbsolutePath(); + } + catch (InvalidPathException | IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not determine real path of resource " + + (resource != null ? resource : resourceLocation), ex); + } + return null; + } + } + + @Override + public String getServerInfo() { + return "MockServletContext"; + } + + @Override + public String getInitParameter(String name) { + Assert.notNull(name, "Parameter name must not be null"); + return this.initParameters.get(name); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(this.initParameters.keySet()); + } + + @Override + public boolean setInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + if (this.initParameters.containsKey(name)) { + return false; + } + this.initParameters.put(name, value); + return true; + } + + public void addInitParameter(String name, String value) { + Assert.notNull(name, "Parameter name must not be null"); + this.initParameters.put(name, value); + } + + @Override + @Nullable + public Object getAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + return this.attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(new LinkedHashSet<>(this.attributes.keySet())); + } + + @Override + public void setAttribute(String name, @Nullable Object value) { + Assert.notNull(name, "Attribute name must not be null"); + if (value != null) { + this.attributes.put(name, value); + } + else { + this.attributes.remove(name); + } + } + + @Override + public void removeAttribute(String name) { + Assert.notNull(name, "Attribute name must not be null"); + this.attributes.remove(name); + } + + public void setServletContextName(String servletContextName) { + this.servletContextName = servletContextName; + } + + @Override + public String getServletContextName() { + return this.servletContextName; + } + + @Override + @Nullable + public ClassLoader getClassLoader() { + return ClassUtils.getDefaultClassLoader(); + } + + @Override + public void declareRoles(String... roleNames) { + Assert.notNull(roleNames, "Role names array must not be null"); + for (String roleName : roleNames) { + Assert.hasLength(roleName, "Role name must not be empty"); + this.declaredRoles.add(roleName); + } + } + + public Set getDeclaredRoles() { + return Collections.unmodifiableSet(this.declaredRoles); + } + + @Override + public void setSessionTrackingModes(Set sessionTrackingModes) + throws IllegalStateException, IllegalArgumentException { + this.sessionTrackingModes = sessionTrackingModes; + } + + @Override + public Set getDefaultSessionTrackingModes() { + return DEFAULT_SESSION_TRACKING_MODES; + } + + @Override + public Set getEffectiveSessionTrackingModes() { + return (this.sessionTrackingModes != null ? + Collections.unmodifiableSet(this.sessionTrackingModes) : DEFAULT_SESSION_TRACKING_MODES); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + return this.sessionCookieConfig; + } + + @Override // on Servlet 4.0 + public void setSessionTimeout(int sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + @Override // on Servlet 4.0 + public int getSessionTimeout() { + return this.sessionTimeout; + } + + @Override // on Servlet 4.0 + public void setRequestCharacterEncoding(@Nullable String requestCharacterEncoding) { + this.requestCharacterEncoding = requestCharacterEncoding; + } + + @Override // on Servlet 4.0 + @Nullable + public String getRequestCharacterEncoding() { + return this.requestCharacterEncoding; + } + + @Override // on Servlet 4.0 + public void setResponseCharacterEncoding(@Nullable String responseCharacterEncoding) { + this.responseCharacterEncoding = responseCharacterEncoding; + } + + @Override // on Servlet 4.0 + @Nullable + public String getResponseCharacterEncoding() { + return this.responseCharacterEncoding; + } + + + //--------------------------------------------------------------------- + // Unsupported Servlet 3.0 registration methods + //--------------------------------------------------------------------- + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + throw new UnsupportedOperationException(); + } + + @Override // on Servlet 4.0 + public ServletRegistration.Dynamic addJspFile(String servletName, String jspFile) { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, String className) { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { + throw new UnsupportedOperationException(); + } + + @Override + public ServletRegistration.Dynamic addServlet(String servletName, Class servletClass) { + throw new UnsupportedOperationException(); + } + + @Override + public T createServlet(Class c) throws ServletException { + throw new UnsupportedOperationException(); + } + + /** + * This method always returns {@code null}. + * @see jakarta.servlet.ServletContext#getServletRegistration(String) + */ + @Override + @Nullable + public ServletRegistration getServletRegistration(String servletName) { + return null; + } + + /** + * This method always returns an {@linkplain Collections#emptyMap empty map}. + * @see jakarta.servlet.ServletContext#getServletRegistrations() + */ + @Override + public Map getServletRegistrations() { + return Collections.emptyMap(); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, String className) { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { + throw new UnsupportedOperationException(); + } + + @Override + public FilterRegistration.Dynamic addFilter(String filterName, Class filterClass) { + throw new UnsupportedOperationException(); + } + + @Override + public T createFilter(Class c) throws ServletException { + throw new UnsupportedOperationException(); + } + + /** + * This method always returns {@code null}. + * @see jakarta.servlet.ServletContext#getFilterRegistration(String) + */ + @Override + @Nullable + public FilterRegistration getFilterRegistration(String filterName) { + return null; + } + + /** + * This method always returns an {@linkplain Collections#emptyMap empty map}. + * @see jakarta.servlet.ServletContext#getFilterRegistrations() + */ + @Override + public Map getFilterRegistrations() { + return Collections.emptyMap(); + } + + @Override + public void addListener(Class listenerClass) { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(String className) { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(T t) { + throw new UnsupportedOperationException(); + } + + @Override + public T createListener(Class c) throws ServletException { + throw new UnsupportedOperationException(); + } + + @Override + public String getVirtualServerName() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/spec/java/org/springframework/mock/web/MockSessionCookieConfig.java b/src/spec/java/org/springframework/mock/web/MockSessionCookieConfig.java new file mode 100644 index 00000000..aef856d2 --- /dev/null +++ b/src/spec/java/org/springframework/mock/web/MockSessionCookieConfig.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.mock.web; + +import jakarta.servlet.SessionCookieConfig; +import org.springframework.lang.Nullable; + +/** + * Mock implementation of the {@link jakarta.servlet.SessionCookieConfig} interface. + * + * @author Juergen Hoeller + * @since 4.0 + * @see jakarta.servlet.ServletContext#getSessionCookieConfig() + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public class MockSessionCookieConfig implements SessionCookieConfig { + + @Nullable + private String name; + + @Nullable + private String domain; + + @Nullable + private String path; + + @Nullable + private String comment; + + private boolean httpOnly; + + private boolean secure; + + private int maxAge = -1; + + + @Override + public void setName(@Nullable String name) { + this.name = name; + } + + @Override + @Nullable + public String getName() { + return this.name; + } + + @Override + public void setDomain(@Nullable String domain) { + this.domain = domain; + } + + @Override + @Nullable + public String getDomain() { + return this.domain; + } + + @Override + public void setPath(@Nullable String path) { + this.path = path; + } + + @Override + @Nullable + public String getPath() { + return this.path; + } + + @Override + public void setComment(@Nullable String comment) { + this.comment = comment; + } + + @Override + @Nullable + public String getComment() { + return this.comment; + } + + @Override + public void setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + } + + @Override + public boolean isHttpOnly() { + return this.httpOnly; + } + + @Override + public void setSecure(boolean secure) { + this.secure = secure; + } + + @Override + public boolean isSecure() { + return this.secure; + } + + @Override + public void setMaxAge(int maxAge) { + this.maxAge = maxAge; + } + + @Override + public int getMaxAge() { + return this.maxAge; + } + +} diff --git a/src/spec/java/org/springframework/web/util/WebUtils.java b/src/spec/java/org/springframework/web/util/WebUtils.java new file mode 100644 index 00000000..93d52d67 --- /dev/null +++ b/src/spec/java/org/springframework/web/util/WebUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.util; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletRequestWrapper; +import org.springframework.lang.Nullable; + +/** + * Miscellaneous utilities for web applications. + * Used by various framework classes. + * + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sebastien Deleuze + * @implNote Source copied into jruby-rack from Spring Test 5.3.39 and minimally changed to support Jakarta Servlet API 5, + * while still compiling to Java 8, which is not supported by Spring's support in 6.0+ (targets only Java 17+). + */ +public abstract class WebUtils { + + /** + * Default character encoding to use when {@code request.getCharacterEncoding} + * returns {@code null}, according to the Servlet spec. + * @see ServletRequest#getCharacterEncoding + */ + public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; + + /** + * Standard Servlet spec context attribute that specifies a temporary + * directory for the current web application, of type {@code java.io.File}. + */ + public static final String TEMP_DIR_CONTEXT_ATTRIBUTE = "jakarta.servlet.context.tempdir"; + + /** + * Return an appropriate request object of the specified type, if available, + * unwrapping the given request as far as necessary. + * @param request the servlet request to introspect + * @param requiredType the desired type of request object + * @return the matching request object, or {@code null} if none + * of that type is available + */ + @SuppressWarnings("unchecked") + @Nullable + public static T getNativeRequest(ServletRequest request, @Nullable Class requiredType) { + if (requiredType != null) { + if (requiredType.isInstance(request)) { + return (T) request; + } + else if (request instanceof ServletRequestWrapper) { + return getNativeRequest(((ServletRequestWrapper) request).getRequest(), requiredType); + } + } + return null; + } +} diff --git a/src/spec/ruby/jruby/rack/booter_spec.rb b/src/spec/ruby/jruby/rack/booter_spec.rb index 516210bd..ef030f07 100644 --- a/src/spec/ruby/jruby/rack/booter_spec.rb +++ b/src/spec/ruby/jruby/rack/booter_spec.rb @@ -230,7 +230,7 @@ before :each do # NOTE: this is obviously poor testing but it's easier to let the factory # setup the runtime for us than to hand copy/stub/mock all code involved - servlet_context = javax.servlet.ServletContext.impl do |name, *args| + servlet_context = Java::JakartaServlet::ServletContext.impl do |name, *args| case name.to_sym when :getRealPath then case args.first @@ -277,7 +277,7 @@ before :each do # NOTE: this is obviously poor testing but it's easier to let the factory # setup the runtime for us than to hand copy/stub/mock all code involved - servlet_context = javax.servlet.ServletContext.impl do |name, *args| + servlet_context = Java::JakartaServlet::ServletContext.impl do |name, *args| case name.to_sym when :getRealPath then case args.first diff --git a/src/spec/ruby/jruby/rack/integration_spec.rb b/src/spec/ruby/jruby/rack/integration_spec.rb index eb686262..71adc55a 100644 --- a/src/spec/ruby/jruby/rack/integration_spec.rb +++ b/src/spec/ruby/jruby/rack/integration_spec.rb @@ -28,7 +28,7 @@ ) listener = org.jruby.rack.RackServletContextListener.new - listener.contextInitialized javax.servlet.ServletContextEvent.new(@servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(@servlet_context) rack_factory = @servlet_context.getAttribute("rack.factory") rack_factory.should be_a(RackApplicationFactory) @@ -46,7 +46,7 @@ "run lambda { |env| [ 200, {'Via' => 'JRuby-Rack', 'Content-Type' => 'text/plain'}, 'OK' ] }" ) listener = org.jruby.rack.RackServletContextListener.new - listener.contextInitialized javax.servlet.ServletContextEvent.new(@servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(@servlet_context) @rack_context = @servlet_context.getAttribute("rack.context") @rack_factory = @servlet_context.getAttribute("rack.factory") end @@ -94,7 +94,7 @@ servlet_context.addInitParameter('jruby.max.runtimes', '2') listener = org.jruby.rack.rails.RailsServletContextListener.new - listener.contextInitialized javax.servlet.ServletContextEvent.new(servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(servlet_context) rack_factory = servlet_context.getAttribute("rack.factory") rack_factory.should be_a(RackApplicationFactory) @@ -109,7 +109,7 @@ it "initializes shared (thread-safe) by default" do listener = org.jruby.rack.rails.RailsServletContextListener.new - listener.contextInitialized javax.servlet.ServletContextEvent.new(servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(servlet_context) rack_factory = servlet_context.getAttribute("rack.factory") rack_factory.should be_a(RackApplicationFactory) @@ -123,7 +123,7 @@ servlet_context.addInitParameter('jruby.max.runtimes', '1') listener = org.jruby.rack.rails.RailsServletContextListener.new - listener.contextInitialized javax.servlet.ServletContextEvent.new(servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(servlet_context) rack_factory = servlet_context.getAttribute("rack.factory") rack_factory.should be_a(RackApplicationFactory) @@ -217,7 +217,7 @@ def initialize_rails(env = nil, servlet_context = @servlet_context) yield(servlet_context, listener) if block_given? - listener.contextInitialized javax.servlet.ServletContextEvent.new(servlet_context) + listener.contextInitialized Java::JakartaServlet::ServletContextEvent.new(servlet_context) @rack_context = servlet_context.getAttribute("rack.context") @rack_factory = servlet_context.getAttribute("rack.factory") @servlet_context = servlet_context diff --git a/src/spec/ruby/jruby/rack/response_spec.rb b/src/spec/ruby/jruby/rack/response_spec.rb index 7de11aa0..13a83d02 100644 --- a/src/spec/ruby/jruby/rack/response_spec.rb +++ b/src/spec/ruby/jruby/rack/response_spec.rb @@ -16,7 +16,7 @@ JRuby::Rack::Response.new [ status, headers, body ] end - let(:servlet_response) { javax.servlet.http.HttpServletResponse.impl {} } + let(:servlet_response) { Java::JakartaServletHttp::HttpServletResponse.impl {} } let(:response_environment) { new_response_environment(servlet_response) } diff --git a/src/spec/ruby/jruby/rack/servlet_ext_spec.rb b/src/spec/ruby/jruby/rack/servlet_ext_spec.rb index 17dcae19..4fd213f8 100644 --- a/src/spec/ruby/jruby/rack/servlet_ext_spec.rb +++ b/src/spec/ruby/jruby/rack/servlet_ext_spec.rb @@ -80,11 +80,11 @@ end - describe Java::JavaxServlet::ServletContext do + describe Java::JakartaServlet::ServletContext do let(:subject) do context = org.springframework.mock.web.MockServletContext.new - context.removeAttribute("javax.servlet.context.tempdir") + context.removeAttribute("jakarta.servlet.context.tempdir") context end @@ -92,10 +92,10 @@ end - describe Java::JavaxServlet::ServletRequest do + describe Java::JakartaServlet::ServletRequest do before :each do - @request = Java::JavaxServlet::ServletRequest.impl {} + @request = Java::JakartaServlet::ServletRequest.impl {} end it "should allow #[] to access request attributes" do @@ -125,10 +125,10 @@ end - describe Java::JavaxServletHttp::HttpSession do + describe Java::JakartaServletHttp::HttpSession do before :each do - @session = Java::JavaxServletHttp::HttpSession.impl {} + @session = Java::JakartaServletHttp::HttpSession.impl {} end it "should allow #[] to access session attributes" do diff --git a/src/spec/ruby/rack/embed/filter_spec.rb b/src/spec/ruby/rack/embed/filter_spec.rb index e2f8b95d..97604430 100644 --- a/src/spec/ruby/rack/embed/filter_spec.rb +++ b/src/spec/ruby/rack/embed/filter_spec.rb @@ -10,11 +10,11 @@ let(:chain) { double "filter chain" } let(:request) do - javax.servlet.http.HttpServletRequest.impl {}.tap do |request| + Java::JakartaServletHttp::HttpServletRequest.impl {}.tap do |request| request.stub(:getInputStream).and_return(StubServletInputStream.new) end end - let(:response) { javax.servlet.http.HttpServletResponse.impl {} } + let(:response) { Java::JakartaServletHttp::HttpServletResponse.impl {} } it "serves all requests using the given rack application" do rack_response = double "rack response" diff --git a/src/spec/ruby/rack/filter_spec.rb b/src/spec/ruby/rack/filter_spec.rb index e5a99109..d2052b0e 100644 --- a/src/spec/ruby/rack/filter_spec.rb +++ b/src/spec/ruby/rack/filter_spec.rb @@ -14,7 +14,7 @@ let(:chain) { double "filter chain" } def stub_request(path_info) - @request = javax.servlet.http.HttpServletRequest.impl {} + @request = Java::JakartaServletHttp::HttpServletRequest.impl {} @request.stub(:setAttribute) if block_given? yield @request, path_info @@ -27,7 +27,7 @@ def stub_request(path_info) before :each do stub_request("/index") - @response = javax.servlet.http.HttpServletResponse.impl {} + @response = Java::JakartaServletHttp::HttpServletResponse.impl {} @rack_context.stub(:getResource).and_return nil @rack_config.stub(:getProperty) do |key, default| ( key || raise("missing key") ) && default @@ -337,7 +337,7 @@ def isHandled(arg); getStatus < 400; end end it "configures not handled statuses on init" do - servlet_context = javax.servlet.ServletContext.impl do |name, *args| + servlet_context = Java::JakartaServlet::ServletContext.impl do |name, *args| case name.to_sym when :getAttribute if args[0] == "rack.context" @@ -347,7 +347,7 @@ def isHandled(arg); getStatus < 400; end nil end end - config = javax.servlet.FilterConfig.impl do |name, *args| + config = Java::JakartaServlet::FilterConfig.impl do |name, *args| case name.to_sym when :getServletContext then servlet_context when :getInitParameter diff --git a/src/spec/ruby/rack/handler/servlet_spec.rb b/src/spec/ruby/rack/handler/servlet_spec.rb index b24d7cd0..07ffbc00 100644 --- a/src/spec/ruby/rack/handler/servlet_spec.rb +++ b/src/spec/ruby/rack/handler/servlet_spec.rb @@ -324,7 +324,7 @@ def _env; @_env end it "exposes the servlet context xxxx" do env = servlet.create_env @servlet_env - expect( env['java.servlet_context'] ).to be_a javax.servlet.ServletContext + expect( env['java.servlet_context'] ).to be_a Java::JakartaServlet::ServletContext end it "exposes the rack context" do @@ -881,24 +881,7 @@ def servlet.create_env(servlet_env) it "returns the servlet context when queried with java.servlet_context" do env = servlet.create_env @servlet_env - - expect( env['java.servlet_context'] ).to_not be nil - if servlet_30? - expect( env['java.servlet_context'] ).to be @servlet_context - else - expect( env['java.servlet_context'] ).to be @rack_context - - # HACK to emulate Servlet API 3.0 MockHttpServletRequest has getServletContext : - env = Rack::Handler::Servlet::DefaultEnv.new(@servlet_request).to_hash - - expect( env['java.servlet_context'] ).to_not be nil - expect( env['java.servlet_context'] ).to be @servlet_context - begin - env['java.servlet_context'].should == @servlet_context - rescue NoMethodError - (env['java.servlet_context'] == @servlet_context).should == true - end - end + expect( env['java.servlet_context'] ).to be @servlet_context end it "returns the servlet request when queried with java.servlet_request" do @@ -1088,9 +1071,9 @@ def servlet.create_env(servlet_env) it "sets cookies from servlet requests" do cookies = [] - cookies << javax.servlet.http.Cookie.new('foo', 'bar') - cookies << javax.servlet.http.Cookie.new('bar', '142') - servlet_request.setCookies cookies.to_java :'javax.servlet.http.Cookie' + cookies << Java::JakartaServletHttp::Cookie.new('foo', 'bar') + cookies << Java::JakartaServletHttp::Cookie.new('bar', '142') + servlet_request.setCookies cookies.to_java :'jakarta.servlet.http.Cookie' env = servlet.create_env(servlet_env) rack_request = Rack::Request.new(env) rack_request.cookies.should == { 'foo' => 'bar', 'bar' => '142' } @@ -1102,7 +1085,7 @@ def servlet.create_env(servlet_env) rack_request = Rack::Request.new(env) rack_request.cookies.should == {} - servlet_request.setCookies [].to_java :'javax.servlet.http.Cookie' + servlet_request.setCookies [].to_java :'jakarta.servlet.http.Cookie' env = servlet.create_env(servlet_env) rack_request = Rack::Request.new(env) rack_request.cookies.should == {} @@ -1110,9 +1093,9 @@ def servlet.create_env(servlet_env) it "sets a single cookie from servlet requests" do cookies = [] - cookies << javax.servlet.http.Cookie.new('foo', 'bar') - cookies << javax.servlet.http.Cookie.new('foo', '142') - servlet_request.setCookies cookies.to_java :'javax.servlet.http.Cookie' + cookies << Java::JakartaServletHttp::Cookie.new('foo', 'bar') + cookies << Java::JakartaServletHttp::Cookie.new('foo', '142') + servlet_request.setCookies cookies.to_java :'jakarta.servlet.http.Cookie' env = servlet.create_env(servlet_env) rack_request = Rack::Request.new(env) rack_request.cookies.should == { 'foo' => 'bar' } diff --git a/src/spec/ruby/rack/servlet/response_capture_spec.rb b/src/spec/ruby/rack/servlet/response_capture_spec.rb index b106b9b9..4cdb5cb3 100644 --- a/src/spec/ruby/rack/servlet/response_capture_spec.rb +++ b/src/spec/ruby/rack/servlet/response_capture_spec.rb @@ -76,8 +76,6 @@ end it "is considered handled when more than Allow header is added with OPTIONS" do - pending "need Servlet API 3.0" unless servlet_30? - servlet_request.method = 'OPTIONS' response_capture.setIntHeader "Answer", 42 @@ -87,8 +85,6 @@ end it "is considered handled when header is added" do - pending "need Servlet API 3.0" unless servlet_30? - servlet_request.method = 'OPTIONS' response_capture.addHeader "Hello", "World" diff --git a/src/spec/ruby/rack/servlet_context_listener_spec.rb b/src/spec/ruby/rack/servlet_context_listener_spec.rb index b2df1708..9374e053 100644 --- a/src/spec/ruby/rack/servlet_context_listener_spec.rb +++ b/src/spec/ruby/rack/servlet_context_listener_spec.rb @@ -16,7 +16,7 @@ end let(:servlet_context_event) do - javax.servlet.ServletContextEvent.new @servlet_context + Java::JakartaServlet::ServletContextEvent.new @servlet_context end describe "contextInitialized" do diff --git a/src/spec/ruby/rack/servlet_spec.rb b/src/spec/ruby/rack/servlet_spec.rb index 8b0f7cd3..9e15a92d 100644 --- a/src/spec/ruby/rack/servlet_spec.rb +++ b/src/spec/ruby/rack/servlet_spec.rb @@ -10,8 +10,8 @@ describe org.jruby.rack.RackServlet, "service" do it "should delegate to process" do - request = javax.servlet.http.HttpServletRequest.impl {} - response = javax.servlet.http.HttpServletResponse.impl {} + request = Java::JakartaServletHttp::HttpServletRequest.impl {} + response = Java::JakartaServletHttp::HttpServletResponse.impl {} dispatcher = double "dispatcher" dispatcher.should_receive(:process) servlet = org.jruby.rack.RackServlet.new dispatcher, @rack_context diff --git a/src/spec/ruby/rack/tag_spec.rb b/src/spec/ruby/rack/tag_spec.rb index 704a6303..97813fe7 100644 --- a/src/spec/ruby/rack/tag_spec.rb +++ b/src/spec/ruby/rack/tag_spec.rb @@ -58,7 +58,7 @@ def call(request) begin @tag.doEndTag - rescue Java::JavaxServletJsp::JspException + rescue Java::JakartaServletJsp::JspException #noop end end diff --git a/src/spec/ruby/spec_helper.rb b/src/spec/ruby/spec_helper.rb index fd9c8dbb..6e6c89e4 100644 --- a/src/spec/ruby/spec_helper.rb +++ b/src/spec/ruby/spec_helper.rb @@ -5,8 +5,8 @@ $CLASSPATH << File.expand_path('test-classes', target) jars.each { |jar| $CLASSPATH << File.expand_path(jar, lib) } -java_import 'javax.servlet.http.HttpServletRequest' -java_import 'javax.servlet.http.HttpServletResponse' +java_import 'jakarta.servlet.http.HttpServletRequest' +java_import 'jakarta.servlet.http.HttpServletResponse' java_import 'org.jruby.rack.RackApplicationFactory' java_import 'org.jruby.rack.DefaultRackApplicationFactory' @@ -24,8 +24,8 @@ module SharedHelpers java_import 'org.jruby.rack.RackContext' java_import 'org.jruby.rack.RackConfig' java_import 'org.jruby.rack.servlet.ServletRackContext' - java_import 'javax.servlet.ServletContext' - java_import 'javax.servlet.ServletConfig' + java_import 'jakarta.servlet.ServletContext' + java_import 'jakarta.servlet.ServletConfig' def mock_servlet_context @servlet_context = ServletContext.impl {} @@ -52,16 +52,8 @@ def silence_warnings(&block) JRuby::Rack::Helpers.silence_warnings(&block) end - @@servlet_30 = nil - - def servlet_30? - return @@servlet_30 unless @@servlet_30.nil? - @@servlet_30 = !! ( Java::javax.servlet.AsyncContext rescue nil ) - end - private :servlet_30? - def rack_release_at_least?(at_least = nil) - require 'rack'; + require 'rack' at_least ? Rack.release >= at_least : true end private :rack_release_at_least? @@ -196,10 +188,10 @@ def should_not_eval_as_nil(code, runtime = @runtime) # alias end -java_import org.springframework.mock.web.MockServletConfig -java_import org.springframework.mock.web.MockServletContext -java_import org.springframework.mock.web.MockHttpServletRequest -java_import org.springframework.mock.web.MockHttpServletResponse +java_import 'org.springframework.mock.web.MockServletConfig' +java_import 'org.springframework.mock.web.MockServletContext' +java_import 'org.springframework.mock.web.MockHttpServletRequest' +java_import 'org.springframework.mock.web.MockHttpServletResponse' class StubInputStream < java.io.InputStream @@ -237,7 +229,7 @@ def flush; end end -class StubServletInputStream < javax.servlet.ServletInputStream +class StubServletInputStream < Java::JakartaServlet::ServletInputStream def initialize(val = "") @delegate = StubInputStream.new(val)