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}