001/* 002 * Copyright 2002-2019 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.test.web.servlet.htmlunit; 018 019import java.io.IOException; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.Map; 023 024import com.gargoylesoftware.htmlunit.CookieManager; 025import com.gargoylesoftware.htmlunit.WebClient; 026import com.gargoylesoftware.htmlunit.WebConnection; 027import com.gargoylesoftware.htmlunit.WebRequest; 028import com.gargoylesoftware.htmlunit.WebResponse; 029import com.gargoylesoftware.htmlunit.util.Cookie; 030import org.apache.http.impl.cookie.BasicClientCookie; 031 032import org.springframework.lang.Nullable; 033import org.springframework.mock.web.MockHttpServletResponse; 034import org.springframework.mock.web.MockHttpSession; 035import org.springframework.test.web.servlet.MockMvc; 036import org.springframework.test.web.servlet.RequestBuilder; 037import org.springframework.test.web.servlet.ResultActions; 038import org.springframework.util.Assert; 039 040/** 041 * {@code MockMvcWebConnection} enables {@link MockMvc} to transform a 042 * {@link WebRequest} into a {@link WebResponse}. 043 * <p>This is the core integration with <a href="http://htmlunit.sourceforge.net/">HtmlUnit</a>. 044 * <p>Example usage can be seen below. 045 * 046 * <pre class="code"> 047 * WebClient webClient = new WebClient(); 048 * MockMvc mockMvc = ... 049 * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc, webClient); 050 * webClient.setWebConnection(webConnection); 051 * 052 * // Use webClient as normal ... 053 * </pre> 054 * 055 * @author Rob Winch 056 * @author Sam Brannen 057 * @since 4.2 058 * @see org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver 059 */ 060public final class MockMvcWebConnection implements WebConnection { 061 062 private final Map<String, MockHttpSession> sessions = new HashMap<>(); 063 064 private final MockMvc mockMvc; 065 066 @Nullable 067 private final String contextPath; 068 069 private WebClient webClient; 070 071 072 /** 073 * Create a new instance that assumes the context path of the application 074 * is {@code ""} (i.e., the root context). 075 * <p>For example, the URL {@code http://localhost/test/this} would use 076 * {@code ""} as the context path. 077 * @param mockMvc the {@code MockMvc} instance to use; never {@code null} 078 * @param webClient the {@link WebClient} to use. never {@code null} 079 */ 080 public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient) { 081 this(mockMvc, webClient, ""); 082 } 083 084 /** 085 * Create a new instance with the specified context path. 086 * <p>The path may be {@code null} in which case the first path segment 087 * of the URL is turned into the contextPath. Otherwise it must conform 088 * to {@link javax.servlet.http.HttpServletRequest#getContextPath()} 089 * which states that it can be an empty string and otherwise must start 090 * with a "/" character and not end with a "/" character. 091 * @param mockMvc the {@code MockMvc} instance to use (never {@code null}) 092 * @param webClient the {@link WebClient} to use (never {@code null}) 093 * @param contextPath the contextPath to use 094 */ 095 public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient, @Nullable String contextPath) { 096 Assert.notNull(mockMvc, "MockMvc must not be null"); 097 Assert.notNull(webClient, "WebClient must not be null"); 098 validateContextPath(contextPath); 099 100 this.webClient = webClient; 101 this.mockMvc = mockMvc; 102 this.contextPath = contextPath; 103 } 104 105 /** 106 * Validate the supplied {@code contextPath}. 107 * <p>If the value is not {@code null}, it must conform to 108 * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which 109 * states that it can be an empty string and otherwise must start with 110 * a "/" character and not end with a "/" character. 111 * @param contextPath the path to validate 112 */ 113 static void validateContextPath(@Nullable String contextPath) { 114 if (contextPath == null || contextPath.isEmpty()) { 115 return; 116 } 117 Assert.isTrue(contextPath.startsWith("/"), () -> "contextPath '" + contextPath + "' must start with '/'."); 118 Assert.isTrue(!contextPath.endsWith("/"), () -> "contextPath '" + contextPath + "' must not end with '/'."); 119 } 120 121 122 public void setWebClient(WebClient webClient) { 123 Assert.notNull(webClient, "WebClient must not be null"); 124 this.webClient = webClient; 125 } 126 127 128 @Override 129 public WebResponse getResponse(WebRequest webRequest) throws IOException { 130 long startTime = System.currentTimeMillis(); 131 HtmlUnitRequestBuilder requestBuilder = new HtmlUnitRequestBuilder(this.sessions, this.webClient, webRequest); 132 requestBuilder.setContextPath(this.contextPath); 133 134 MockHttpServletResponse httpServletResponse = getResponse(requestBuilder); 135 String forwardedUrl = httpServletResponse.getForwardedUrl(); 136 while (forwardedUrl != null) { 137 requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl)); 138 httpServletResponse = getResponse(requestBuilder); 139 forwardedUrl = httpServletResponse.getForwardedUrl(); 140 } 141 storeCookies(webRequest, httpServletResponse.getCookies()); 142 143 return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build(); 144 } 145 146 private MockHttpServletResponse getResponse(RequestBuilder requestBuilder) throws IOException { 147 ResultActions resultActions; 148 try { 149 resultActions = this.mockMvc.perform(requestBuilder); 150 } 151 catch (Exception ex) { 152 throw new IOException(ex); 153 } 154 155 return resultActions.andReturn().getResponse(); 156 } 157 158 private void storeCookies(WebRequest webRequest, javax.servlet.http.Cookie[] cookies) { 159 Date now = new Date(); 160 CookieManager cookieManager = this.webClient.getCookieManager(); 161 for (javax.servlet.http.Cookie cookie : cookies) { 162 if (cookie.getDomain() == null) { 163 cookie.setDomain(webRequest.getUrl().getHost()); 164 } 165 Cookie toManage = createCookie(cookie); 166 Date expires = toManage.getExpires(); 167 if (expires == null || expires.after(now)) { 168 cookieManager.addCookie(toManage); 169 } 170 else { 171 cookieManager.removeCookie(toManage); 172 } 173 } 174 } 175 176 private static com.gargoylesoftware.htmlunit.util.Cookie createCookie(javax.servlet.http.Cookie cookie) { 177 Date expires = null; 178 if (cookie.getMaxAge() > -1) { 179 expires = new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000); 180 } 181 BasicClientCookie result = new BasicClientCookie(cookie.getName(), cookie.getValue()); 182 result.setDomain(cookie.getDomain()); 183 result.setComment(cookie.getComment()); 184 result.setExpiryDate(expires); 185 result.setPath(cookie.getPath()); 186 result.setSecure(cookie.getSecure()); 187 if (cookie.isHttpOnly()) { 188 result.setAttribute("httponly", "true"); 189 } 190 return new com.gargoylesoftware.htmlunit.util.Cookie(result); 191 } 192 193 @Override 194 public void close() { 195 } 196 197}