001/* 002 * Copyright 2002-2016 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.mock.web; 018 019import java.io.File; 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.MalformedURLException; 023import java.net.URL; 024import java.util.Collections; 025import java.util.Enumeration; 026import java.util.EventListener; 027import java.util.HashMap; 028import java.util.LinkedHashMap; 029import java.util.LinkedHashSet; 030import java.util.Map; 031import java.util.Set; 032import javax.activation.FileTypeMap; 033import javax.servlet.Filter; 034import javax.servlet.FilterRegistration; 035import javax.servlet.RequestDispatcher; 036import javax.servlet.Servlet; 037import javax.servlet.ServletContext; 038import javax.servlet.ServletException; 039import javax.servlet.ServletRegistration; 040import javax.servlet.SessionCookieConfig; 041import javax.servlet.SessionTrackingMode; 042import javax.servlet.descriptor.JspConfigDescriptor; 043 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046 047import org.springframework.core.io.DefaultResourceLoader; 048import org.springframework.core.io.Resource; 049import org.springframework.core.io.ResourceLoader; 050import org.springframework.util.Assert; 051import org.springframework.util.ClassUtils; 052import org.springframework.util.ObjectUtils; 053import org.springframework.web.util.WebUtils; 054 055/** 056 * Mock implementation of the {@link javax.servlet.ServletContext} interface. 057 * 058 * <p>As of Spring 4.0, this set of mocks is designed on a Servlet 3.0 baseline. 059 * 060 * <p>Compatible with Servlet 3.0 but can be configured to expose a specific version 061 * through {@link #setMajorVersion}/{@link #setMinorVersion}; default is 3.0. 062 * Note that Servlet 3.0 support is limited: servlet, filter and listener 063 * registration methods are not supported; neither is JSP configuration. 064 * We generally do not recommend to unit test your ServletContainerInitializers and 065 * WebApplicationInitializers which is where those registration methods would be used. 066 * 067 * <p>Used for testing the Spring web framework; only rarely necessary for testing 068 * application controllers. As long as application components don't explicitly 069 * access the {@code ServletContext}, {@code ClassPathXmlApplicationContext} or 070 * {@code FileSystemXmlApplicationContext} can be used to load the context files 071 * for testing, even for {@code DispatcherServlet} context definitions. 072 * 073 * <p>For setting up a full {@code WebApplicationContext} in a test environment, 074 * you can use {@code AnnotationConfigWebApplicationContext}, 075 * {@code XmlWebApplicationContext}, or {@code GenericWebApplicationContext}, 076 * passing in an appropriate {@code MockServletContext} instance. You might want 077 * to configure your {@code MockServletContext} with a {@code FileSystemResourceLoader} 078 * in that case to ensure that resource paths are interpreted as relative filesystem 079 * locations. 080 * 081 * <p>A common setup is to point your JVM working directory to the root of your 082 * web application directory, in combination with filesystem-based resource loading. 083 * This allows to load the context files as used in the web application, with 084 * relative paths getting interpreted correctly. Such a setup will work with both 085 * {@code FileSystemXmlApplicationContext} (which will load straight from the 086 * filesystem) and {@code XmlWebApplicationContext} with an underlying 087 * {@code MockServletContext} (as long as the {@code MockServletContext} has been 088 * configured with a {@code FileSystemResourceLoader}). 089 * 090 * @author Rod Johnson 091 * @author Juergen Hoeller 092 * @author Sam Brannen 093 * @since 1.0.2 094 * @see #MockServletContext(org.springframework.core.io.ResourceLoader) 095 * @see org.springframework.web.context.support.AnnotationConfigWebApplicationContext 096 * @see org.springframework.web.context.support.XmlWebApplicationContext 097 * @see org.springframework.web.context.support.GenericWebApplicationContext 098 * @see org.springframework.context.support.ClassPathXmlApplicationContext 099 * @see org.springframework.context.support.FileSystemXmlApplicationContext 100 */ 101public class MockServletContext implements ServletContext { 102 103 /** Default Servlet name used by Tomcat, Jetty, JBoss, and GlassFish: {@value}. */ 104 private static final String COMMON_DEFAULT_SERVLET_NAME = "default"; 105 106 private static final String TEMP_DIR_SYSTEM_PROPERTY = "java.io.tmpdir"; 107 108 private static final Set<SessionTrackingMode> DEFAULT_SESSION_TRACKING_MODES = 109 new LinkedHashSet<SessionTrackingMode>(3); 110 111 static { 112 DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.COOKIE); 113 DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.URL); 114 DEFAULT_SESSION_TRACKING_MODES.add(SessionTrackingMode.SSL); 115 } 116 117 118 private final Log logger = LogFactory.getLog(getClass()); 119 120 private final ResourceLoader resourceLoader; 121 122 private final String resourceBasePath; 123 124 private String contextPath = ""; 125 126 private final Map<String, ServletContext> contexts = new HashMap<String, ServletContext>(); 127 128 private int majorVersion = 3; 129 130 private int minorVersion = 0; 131 132 private int effectiveMajorVersion = 3; 133 134 private int effectiveMinorVersion = 0; 135 136 private final Map<String, RequestDispatcher> namedRequestDispatchers = new HashMap<String, RequestDispatcher>(); 137 138 private String defaultServletName = COMMON_DEFAULT_SERVLET_NAME; 139 140 private final Map<String, String> initParameters = new LinkedHashMap<String, String>(); 141 142 private final Map<String, Object> attributes = new LinkedHashMap<String, Object>(); 143 144 private String servletContextName = "MockServletContext"; 145 146 private final Set<String> declaredRoles = new LinkedHashSet<String>(); 147 148 private Set<SessionTrackingMode> sessionTrackingModes; 149 150 private final SessionCookieConfig sessionCookieConfig = new MockSessionCookieConfig(); 151 152 153 /** 154 * Create a new {@code MockServletContext}, using no base path and a 155 * {@link DefaultResourceLoader} (i.e. the classpath root as WAR root). 156 * @see org.springframework.core.io.DefaultResourceLoader 157 */ 158 public MockServletContext() { 159 this("", null); 160 } 161 162 /** 163 * Create a new {@code MockServletContext}, using a {@link DefaultResourceLoader}. 164 * @param resourceBasePath the root directory of the WAR (should not end with a slash) 165 * @see org.springframework.core.io.DefaultResourceLoader 166 */ 167 public MockServletContext(String resourceBasePath) { 168 this(resourceBasePath, null); 169 } 170 171 /** 172 * Create a new {@code MockServletContext}, using the specified {@link ResourceLoader} 173 * and no base path. 174 * @param resourceLoader the ResourceLoader to use (or null for the default) 175 */ 176 public MockServletContext(ResourceLoader resourceLoader) { 177 this("", resourceLoader); 178 } 179 180 /** 181 * Create a new {@code MockServletContext} using the supplied resource base 182 * path and resource loader. 183 * <p>Registers a {@link MockRequestDispatcher} for the Servlet named 184 * {@literal 'default'}. 185 * @param resourceBasePath the root directory of the WAR (should not end with a slash) 186 * @param resourceLoader the ResourceLoader to use (or null for the default) 187 * @see #registerNamedDispatcher 188 */ 189 public MockServletContext(String resourceBasePath, ResourceLoader resourceLoader) { 190 this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); 191 this.resourceBasePath = (resourceBasePath != null ? resourceBasePath : ""); 192 193 // Use JVM temp dir as ServletContext temp dir. 194 String tempDir = System.getProperty(TEMP_DIR_SYSTEM_PROPERTY); 195 if (tempDir != null) { 196 this.attributes.put(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE, new File(tempDir)); 197 } 198 199 registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); 200 } 201 202 /** 203 * Build a full resource location for the given path, prepending the resource 204 * base path of this {@code MockServletContext}. 205 * @param path the path as specified 206 * @return the full resource path 207 */ 208 protected String getResourceLocation(String path) { 209 if (!path.startsWith("/")) { 210 path = "/" + path; 211 } 212 return this.resourceBasePath + path; 213 } 214 215 public void setContextPath(String contextPath) { 216 this.contextPath = (contextPath != null ? contextPath : ""); 217 } 218 219 @Override 220 public String getContextPath() { 221 return this.contextPath; 222 } 223 224 public void registerContext(String contextPath, ServletContext context) { 225 this.contexts.put(contextPath, context); 226 } 227 228 @Override 229 public ServletContext getContext(String contextPath) { 230 if (this.contextPath.equals(contextPath)) { 231 return this; 232 } 233 return this.contexts.get(contextPath); 234 } 235 236 public void setMajorVersion(int majorVersion) { 237 this.majorVersion = majorVersion; 238 } 239 240 @Override 241 public int getMajorVersion() { 242 return this.majorVersion; 243 } 244 245 public void setMinorVersion(int minorVersion) { 246 this.minorVersion = minorVersion; 247 } 248 249 @Override 250 public int getMinorVersion() { 251 return this.minorVersion; 252 } 253 254 public void setEffectiveMajorVersion(int effectiveMajorVersion) { 255 this.effectiveMajorVersion = effectiveMajorVersion; 256 } 257 258 @Override 259 public int getEffectiveMajorVersion() { 260 return this.effectiveMajorVersion; 261 } 262 263 public void setEffectiveMinorVersion(int effectiveMinorVersion) { 264 this.effectiveMinorVersion = effectiveMinorVersion; 265 } 266 267 @Override 268 public int getEffectiveMinorVersion() { 269 return this.effectiveMinorVersion; 270 } 271 272 /** 273 * This method uses the default 274 * {@link javax.activation.FileTypeMap#getDefaultFileTypeMap() FileTypeMap} 275 * from the Java Activation Framework to resolve MIME types. 276 * <p>The Java Activation Framework returns {@code "application/octet-stream"} 277 * if the MIME type is unknown (i.e., it never returns {@code null}). Thus, in 278 * order to honor the {@link ServletContext#getMimeType(String)} contract, 279 * this method returns {@code null} if the MIME type is 280 * {@code "application/octet-stream"}. 281 * <p>{@code MockServletContext} does not provide a direct mechanism for 282 * setting a custom MIME type; however, if the default {@code FileTypeMap} 283 * is an instance of {@code javax.activation.MimetypesFileTypeMap}, a custom 284 * MIME type named {@code text/enigma} can be registered for a custom 285 * {@code .puzzle} file extension in the following manner: 286 * <pre style="code"> 287 * MimetypesFileTypeMap mimetypesFileTypeMap = (MimetypesFileTypeMap) FileTypeMap.getDefaultFileTypeMap(); 288 * mimetypesFileTypeMap.addMimeTypes("text/enigma puzzle"); 289 * </pre> 290 */ 291 @Override 292 public String getMimeType(String filePath) { 293 String mimeType = FileTypeMap.getDefaultFileTypeMap().getContentType(filePath); 294 return ("application/octet-stream".equals(mimeType) ? null : mimeType); 295 } 296 297 @Override 298 public Set<String> getResourcePaths(String path) { 299 String actualPath = (path.endsWith("/") ? path : path + "/"); 300 Resource resource = this.resourceLoader.getResource(getResourceLocation(actualPath)); 301 try { 302 File file = resource.getFile(); 303 String[] fileList = file.list(); 304 if (ObjectUtils.isEmpty(fileList)) { 305 return null; 306 } 307 Set<String> resourcePaths = new LinkedHashSet<String>(fileList.length); 308 for (String fileEntry : fileList) { 309 String resultPath = actualPath + fileEntry; 310 if (resource.createRelative(fileEntry).getFile().isDirectory()) { 311 resultPath += "/"; 312 } 313 resourcePaths.add(resultPath); 314 } 315 return resourcePaths; 316 } 317 catch (IOException ex) { 318 logger.warn("Couldn't get resource paths for " + resource, ex); 319 return null; 320 } 321 } 322 323 @Override 324 public URL getResource(String path) throws MalformedURLException { 325 Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); 326 if (!resource.exists()) { 327 return null; 328 } 329 try { 330 return resource.getURL(); 331 } 332 catch (MalformedURLException ex) { 333 throw ex; 334 } 335 catch (IOException ex) { 336 logger.warn("Couldn't get URL for " + resource, ex); 337 return null; 338 } 339 } 340 341 @Override 342 public InputStream getResourceAsStream(String path) { 343 Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); 344 if (!resource.exists()) { 345 return null; 346 } 347 try { 348 return resource.getInputStream(); 349 } 350 catch (IOException ex) { 351 logger.warn("Couldn't open InputStream for " + resource, ex); 352 return null; 353 } 354 } 355 356 @Override 357 public RequestDispatcher getRequestDispatcher(String path) { 358 if (!path.startsWith("/")) { 359 throw new IllegalArgumentException("RequestDispatcher path at ServletContext level must start with '/'"); 360 } 361 return new MockRequestDispatcher(path); 362 } 363 364 @Override 365 public RequestDispatcher getNamedDispatcher(String path) { 366 return this.namedRequestDispatchers.get(path); 367 } 368 369 /** 370 * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher}) 371 * that acts as a wrapper for the named Servlet. 372 * @param name the name of the wrapped Servlet 373 * @param requestDispatcher the dispatcher that wraps the named Servlet 374 * @see #getNamedDispatcher 375 * @see #unregisterNamedDispatcher 376 */ 377 public void registerNamedDispatcher(String name, RequestDispatcher requestDispatcher) { 378 Assert.notNull(name, "RequestDispatcher name must not be null"); 379 Assert.notNull(requestDispatcher, "RequestDispatcher must not be null"); 380 this.namedRequestDispatchers.put(name, requestDispatcher); 381 } 382 383 /** 384 * Unregister the {@link RequestDispatcher} with the given name. 385 * @param name the name of the dispatcher to unregister 386 * @see #getNamedDispatcher 387 * @see #registerNamedDispatcher 388 */ 389 public void unregisterNamedDispatcher(String name) { 390 Assert.notNull(name, "RequestDispatcher name must not be null"); 391 this.namedRequestDispatchers.remove(name); 392 } 393 394 /** 395 * Get the name of the <em>default</em> {@code Servlet}. 396 * <p>Defaults to {@literal 'default'}. 397 * @see #setDefaultServletName 398 */ 399 public String getDefaultServletName() { 400 return this.defaultServletName; 401 } 402 403 /** 404 * Set the name of the <em>default</em> {@code Servlet}. 405 * <p>Also {@link #unregisterNamedDispatcher unregisters} the current default 406 * {@link RequestDispatcher} and {@link #registerNamedDispatcher replaces} 407 * it with a {@link MockRequestDispatcher} for the provided 408 * {@code defaultServletName}. 409 * @param defaultServletName the name of the <em>default</em> {@code Servlet}; 410 * never {@code null} or empty 411 * @see #getDefaultServletName 412 */ 413 public void setDefaultServletName(String defaultServletName) { 414 Assert.hasText(defaultServletName, "defaultServletName must not be null or empty"); 415 unregisterNamedDispatcher(this.defaultServletName); 416 this.defaultServletName = defaultServletName; 417 registerNamedDispatcher(this.defaultServletName, new MockRequestDispatcher(this.defaultServletName)); 418 } 419 420 @Override 421 @Deprecated 422 public Servlet getServlet(String name) { 423 return null; 424 } 425 426 @Override 427 @Deprecated 428 public Enumeration<Servlet> getServlets() { 429 return Collections.enumeration(Collections.<Servlet>emptySet()); 430 } 431 432 @Override 433 @Deprecated 434 public Enumeration<String> getServletNames() { 435 return Collections.enumeration(Collections.<String>emptySet()); 436 } 437 438 @Override 439 public void log(String message) { 440 logger.info(message); 441 } 442 443 @Override 444 @Deprecated 445 public void log(Exception ex, String message) { 446 logger.info(message, ex); 447 } 448 449 @Override 450 public void log(String message, Throwable ex) { 451 logger.info(message, ex); 452 } 453 454 @Override 455 public String getRealPath(String path) { 456 Resource resource = this.resourceLoader.getResource(getResourceLocation(path)); 457 try { 458 return resource.getFile().getAbsolutePath(); 459 } 460 catch (IOException ex) { 461 logger.warn("Couldn't determine real path of resource " + resource, ex); 462 return null; 463 } 464 } 465 466 @Override 467 public String getServerInfo() { 468 return "MockServletContext"; 469 } 470 471 @Override 472 public String getInitParameter(String name) { 473 Assert.notNull(name, "Parameter name must not be null"); 474 return this.initParameters.get(name); 475 } 476 477 @Override 478 public Enumeration<String> getInitParameterNames() { 479 return Collections.enumeration(this.initParameters.keySet()); 480 } 481 482 @Override 483 public boolean setInitParameter(String name, String value) { 484 Assert.notNull(name, "Parameter name must not be null"); 485 if (this.initParameters.containsKey(name)) { 486 return false; 487 } 488 this.initParameters.put(name, value); 489 return true; 490 } 491 492 public void addInitParameter(String name, String value) { 493 Assert.notNull(name, "Parameter name must not be null"); 494 this.initParameters.put(name, value); 495 } 496 497 @Override 498 public Object getAttribute(String name) { 499 Assert.notNull(name, "Attribute name must not be null"); 500 return this.attributes.get(name); 501 } 502 503 @Override 504 public Enumeration<String> getAttributeNames() { 505 return Collections.enumeration(new LinkedHashSet<String>(this.attributes.keySet())); 506 } 507 508 @Override 509 public void setAttribute(String name, Object value) { 510 Assert.notNull(name, "Attribute name must not be null"); 511 if (value != null) { 512 this.attributes.put(name, value); 513 } 514 else { 515 this.attributes.remove(name); 516 } 517 } 518 519 @Override 520 public void removeAttribute(String name) { 521 Assert.notNull(name, "Attribute name must not be null"); 522 this.attributes.remove(name); 523 } 524 525 public void setServletContextName(String servletContextName) { 526 this.servletContextName = servletContextName; 527 } 528 529 @Override 530 public String getServletContextName() { 531 return this.servletContextName; 532 } 533 534 @Override 535 public ClassLoader getClassLoader() { 536 return ClassUtils.getDefaultClassLoader(); 537 } 538 539 @Override 540 public void declareRoles(String... roleNames) { 541 Assert.notNull(roleNames, "Role names array must not be null"); 542 for (String roleName : roleNames) { 543 Assert.hasLength(roleName, "Role name must not be empty"); 544 this.declaredRoles.add(roleName); 545 } 546 } 547 548 public Set<String> getDeclaredRoles() { 549 return Collections.unmodifiableSet(this.declaredRoles); 550 } 551 552 @Override 553 public void setSessionTrackingModes(Set<SessionTrackingMode> sessionTrackingModes) 554 throws IllegalStateException, IllegalArgumentException { 555 this.sessionTrackingModes = sessionTrackingModes; 556 } 557 558 @Override 559 public Set<SessionTrackingMode> getDefaultSessionTrackingModes() { 560 return DEFAULT_SESSION_TRACKING_MODES; 561 } 562 563 @Override 564 public Set<SessionTrackingMode> getEffectiveSessionTrackingModes() { 565 return (this.sessionTrackingModes != null ? 566 Collections.unmodifiableSet(this.sessionTrackingModes) : DEFAULT_SESSION_TRACKING_MODES); 567 } 568 569 @Override 570 public SessionCookieConfig getSessionCookieConfig() { 571 return this.sessionCookieConfig; 572 } 573 574 575 //--------------------------------------------------------------------- 576 // Unsupported Servlet 3.0 registration methods 577 //--------------------------------------------------------------------- 578 579 @Override 580 public JspConfigDescriptor getJspConfigDescriptor() { 581 throw new UnsupportedOperationException(); 582 } 583 584 @Override 585 public ServletRegistration.Dynamic addServlet(String servletName, String className) { 586 throw new UnsupportedOperationException(); 587 } 588 589 @Override 590 public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) { 591 throw new UnsupportedOperationException(); 592 } 593 594 @Override 595 public ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) { 596 throw new UnsupportedOperationException(); 597 } 598 599 @Override 600 public <T extends Servlet> T createServlet(Class<T> c) throws ServletException { 601 throw new UnsupportedOperationException(); 602 } 603 604 /** 605 * This method always returns {@code null}. 606 * @see javax.servlet.ServletContext#getServletRegistration(java.lang.String) 607 */ 608 @Override 609 public ServletRegistration getServletRegistration(String servletName) { 610 return null; 611 } 612 613 /** 614 * This method always returns an {@linkplain Collections#emptyMap empty map}. 615 * @see javax.servlet.ServletContext#getServletRegistrations() 616 */ 617 @Override 618 public Map<String, ? extends ServletRegistration> getServletRegistrations() { 619 return Collections.emptyMap(); 620 } 621 622 @Override 623 public FilterRegistration.Dynamic addFilter(String filterName, String className) { 624 throw new UnsupportedOperationException(); 625 } 626 627 @Override 628 public FilterRegistration.Dynamic addFilter(String filterName, Filter filter) { 629 throw new UnsupportedOperationException(); 630 } 631 632 @Override 633 public FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) { 634 throw new UnsupportedOperationException(); 635 } 636 637 @Override 638 public <T extends Filter> T createFilter(Class<T> c) throws ServletException { 639 throw new UnsupportedOperationException(); 640 } 641 642 /** 643 * This method always returns {@code null}. 644 * @see javax.servlet.ServletContext#getFilterRegistration(java.lang.String) 645 */ 646 @Override 647 public FilterRegistration getFilterRegistration(String filterName) { 648 return null; 649 } 650 651 /** 652 * This method always returns an {@linkplain Collections#emptyMap empty map}. 653 * @see javax.servlet.ServletContext#getFilterRegistrations() 654 */ 655 @Override 656 public Map<String, ? extends FilterRegistration> getFilterRegistrations() { 657 return Collections.emptyMap(); 658 } 659 660 @Override 661 public void addListener(Class<? extends EventListener> listenerClass) { 662 throw new UnsupportedOperationException(); 663 } 664 665 @Override 666 public void addListener(String className) { 667 throw new UnsupportedOperationException(); 668 } 669 670 @Override 671 public <T extends EventListener> void addListener(T t) { 672 throw new UnsupportedOperationException(); 673 } 674 675 @Override 676 public <T extends EventListener> T createListener(Class<T> c) throws ServletException { 677 throw new UnsupportedOperationException(); 678 } 679 680}