001/* 002 * Copyright 2002-2017 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; 030 031import org.springframework.mock.web.MockHttpServletResponse; 032import org.springframework.mock.web.MockHttpSession; 033import org.springframework.test.web.servlet.MockMvc; 034import org.springframework.test.web.servlet.RequestBuilder; 035import org.springframework.test.web.servlet.ResultActions; 036import org.springframework.util.Assert; 037 038/** 039 * {@code MockMvcWebConnection} enables {@link MockMvc} to transform a 040 * {@link WebRequest} into a {@link WebResponse}. 041 * <p>This is the core integration with <a href="http://htmlunit.sourceforge.net/">HtmlUnit</a>. 042 * <p>Example usage can be seen below. 043 * 044 * <pre class="code"> 045 * WebClient webClient = new WebClient(); 046 * MockMvc mockMvc = ... 047 * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc, webClient); 048 * webClient.setWebConnection(webConnection); 049 * 050 * // Use webClient as normal ... 051 * </pre> 052 * 053 * @author Rob Winch 054 * @author Sam Brannen 055 * @since 4.2 056 * @see org.springframework.test.web.servlet.htmlunit.webdriver.WebConnectionHtmlUnitDriver 057 */ 058public final class MockMvcWebConnection implements WebConnection { 059 060 private final Map<String, MockHttpSession> sessions = new HashMap<String, MockHttpSession>(); 061 062 private final MockMvc mockMvc; 063 064 private final String contextPath; 065 066 private WebClient webClient; 067 068 069 /** 070 * Create a new instance that assumes the context path of the application 071 * is {@code ""} (i.e., the root context). 072 * <p>For example, the URL {@code http://localhost/test/this} would use 073 * {@code ""} as the context path. 074 * @param mockMvc the {@code MockMvc} instance to use; never {@code null} 075 * @param webClient the {@link WebClient} to use. never {@code null} 076 */ 077 public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient) { 078 this(mockMvc, webClient, ""); 079 } 080 081 /** 082 * Create a new instance with the specified context path. 083 * <p>The path may be {@code null} in which case the first path segment 084 * of the URL is turned into the contextPath. Otherwise it must conform 085 * to {@link javax.servlet.http.HttpServletRequest#getContextPath()} 086 * which states that it can be an empty string and otherwise must start 087 * with a "/" character and not end with a "/" character. 088 * @param mockMvc the {@code MockMvc} instance to use (never {@code null}) 089 * @param webClient the {@link WebClient} to use (never {@code null}) 090 * @param contextPath the contextPath to use 091 */ 092 public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient, String contextPath) { 093 Assert.notNull(mockMvc, "MockMvc must not be null"); 094 Assert.notNull(webClient, "WebClient must not be null"); 095 validateContextPath(contextPath); 096 097 this.webClient = webClient; 098 this.mockMvc = mockMvc; 099 this.contextPath = contextPath; 100 } 101 102 /** 103 * Create a new instance that assumes the context path of the application 104 * is {@code ""} (i.e., the root context). 105 * <p>For example, the URL {@code http://localhost/test/this} would use 106 * {@code ""} as the context path. 107 * @param mockMvc the {@code MockMvc} instance to use; never {@code null} 108 * @deprecated Use {@link #MockMvcWebConnection(MockMvc, WebClient)} 109 */ 110 @Deprecated 111 public MockMvcWebConnection(MockMvc mockMvc) { 112 this(mockMvc, ""); 113 } 114 115 /** 116 * Create a new instance with the specified context path. 117 * <p>The path may be {@code null} in which case the first path segment 118 * of the URL is turned into the contextPath. Otherwise it must conform 119 * to {@link javax.servlet.http.HttpServletRequest#getContextPath()} 120 * which states that it can be an empty string and otherwise must start 121 * with a "/" character and not end with a "/" character. 122 * @param mockMvc the {@code MockMvc} instance to use; never {@code null} 123 * @param contextPath the contextPath to use 124 * @deprecated use {@link #MockMvcWebConnection(MockMvc, WebClient, String)} 125 */ 126 @Deprecated 127 public MockMvcWebConnection(MockMvc mockMvc, String contextPath) { 128 this(mockMvc, new WebClient(), contextPath); 129 } 130 131 /** 132 * Validate the supplied {@code contextPath}. 133 * <p>If the value is not {@code null}, it must conform to 134 * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which 135 * states that it can be an empty string and otherwise must start with 136 * a "/" character and not end with a "/" character. 137 * @param contextPath the path to validate 138 */ 139 static void validateContextPath(String contextPath) { 140 if (contextPath == null || "".equals(contextPath)) { 141 return; 142 } 143 if (!contextPath.startsWith("/")) { 144 throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'."); 145 } 146 if (contextPath.endsWith("/")) { 147 throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'."); 148 } 149 } 150 151 152 public void setWebClient(WebClient webClient) { 153 Assert.notNull(webClient, "WebClient must not be null"); 154 this.webClient = webClient; 155 } 156 157 158 public WebResponse getResponse(WebRequest webRequest) throws IOException { 159 long startTime = System.currentTimeMillis(); 160 HtmlUnitRequestBuilder requestBuilder = new HtmlUnitRequestBuilder(this.sessions, this.webClient, webRequest); 161 requestBuilder.setContextPath(this.contextPath); 162 163 MockHttpServletResponse httpServletResponse = getResponse(requestBuilder); 164 String forwardedUrl = httpServletResponse.getForwardedUrl(); 165 while (forwardedUrl != null) { 166 requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl)); 167 httpServletResponse = getResponse(requestBuilder); 168 forwardedUrl = httpServletResponse.getForwardedUrl(); 169 } 170 storeCookies(webRequest, httpServletResponse.getCookies()); 171 172 return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build(); 173 } 174 175 private MockHttpServletResponse getResponse(RequestBuilder requestBuilder) throws IOException { 176 ResultActions resultActions; 177 try { 178 resultActions = this.mockMvc.perform(requestBuilder); 179 } 180 catch (Exception ex) { 181 throw new IOException(ex); 182 } 183 184 return resultActions.andReturn().getResponse(); 185 } 186 187 private void storeCookies(WebRequest webRequest, javax.servlet.http.Cookie[] cookies) { 188 if (cookies == null) { 189 return; 190 } 191 Date now = new Date(); 192 CookieManager cookieManager = this.webClient.getCookieManager(); 193 for (javax.servlet.http.Cookie cookie : cookies) { 194 if (cookie.getDomain() == null) { 195 cookie.setDomain(webRequest.getUrl().getHost()); 196 } 197 Cookie toManage = MockWebResponseBuilder.createCookie(cookie); 198 Date expires = toManage.getExpires(); 199 if (expires == null || expires.after(now)) { 200 cookieManager.addCookie(toManage); 201 } 202 else { 203 cookieManager.removeCookie(toManage); 204 } 205 } 206 } 207 208 @Override 209 public void close() { 210 } 211 212}