001/*
002 * Copyright 2012-2018 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 *      http://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.boot.web.embedded.tomcat;
018
019import java.io.File;
020import java.io.InputStream;
021import java.lang.reflect.Method;
022import java.net.URL;
023import java.nio.charset.Charset;
024import java.nio.charset.StandardCharsets;
025import java.time.Duration;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Locale;
033import java.util.Set;
034
035import javax.servlet.ServletContainerInitializer;
036
037import org.apache.catalina.Context;
038import org.apache.catalina.Engine;
039import org.apache.catalina.Host;
040import org.apache.catalina.Lifecycle;
041import org.apache.catalina.LifecycleEvent;
042import org.apache.catalina.LifecycleException;
043import org.apache.catalina.LifecycleListener;
044import org.apache.catalina.Manager;
045import org.apache.catalina.Valve;
046import org.apache.catalina.WebResource;
047import org.apache.catalina.WebResourceRoot.ResourceSetType;
048import org.apache.catalina.WebResourceSet;
049import org.apache.catalina.Wrapper;
050import org.apache.catalina.connector.Connector;
051import org.apache.catalina.core.AprLifecycleListener;
052import org.apache.catalina.loader.WebappLoader;
053import org.apache.catalina.session.StandardManager;
054import org.apache.catalina.startup.Tomcat;
055import org.apache.catalina.startup.Tomcat.FixContextListener;
056import org.apache.catalina.util.LifecycleBase;
057import org.apache.catalina.webresources.AbstractResourceSet;
058import org.apache.catalina.webresources.EmptyResource;
059import org.apache.catalina.webresources.StandardRoot;
060import org.apache.coyote.AbstractProtocol;
061import org.apache.coyote.http2.Http2Protocol;
062import org.apache.tomcat.util.scan.StandardJarScanFilter;
063
064import org.springframework.boot.web.server.ErrorPage;
065import org.springframework.boot.web.server.MimeMappings;
066import org.springframework.boot.web.server.WebServer;
067import org.springframework.boot.web.servlet.ServletContextInitializer;
068import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory;
069import org.springframework.context.ResourceLoaderAware;
070import org.springframework.core.io.ResourceLoader;
071import org.springframework.util.Assert;
072import org.springframework.util.ClassUtils;
073import org.springframework.util.ReflectionUtils;
074import org.springframework.util.StringUtils;
075
076/**
077 * {@link AbstractServletWebServerFactory} that can be used to create
078 * {@link TomcatWebServer}s. Can be initialized using Spring's
079 * {@link ServletContextInitializer}s or Tomcat {@link LifecycleListener}s.
080 * <p>
081 * Unless explicitly configured otherwise this factory will create containers that listen
082 * for HTTP requests on port 8080.
083 *
084 * @author Phillip Webb
085 * @author Dave Syer
086 * @author Brock Mills
087 * @author Stephane Nicoll
088 * @author Andy Wilkinson
089 * @author EddĂș MelĂ©ndez
090 * @author Christoffer Sawicki
091 * @since 2.0.0
092 * @see #setPort(int)
093 * @see #setContextLifecycleListeners(Collection)
094 * @see TomcatWebServer
095 */
096public class TomcatServletWebServerFactory extends AbstractServletWebServerFactory
097                implements ConfigurableTomcatWebServerFactory, ResourceLoaderAware {
098
099        private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
100
101        private static final Set<Class<?>> NO_CLASSES = Collections.emptySet();
102
103        /**
104         * The class name of default protocol used.
105         */
106        public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
107
108        private File baseDirectory;
109
110        private List<Valve> engineValves = new ArrayList<>();
111
112        private List<Valve> contextValves = new ArrayList<>();
113
114        private List<LifecycleListener> contextLifecycleListeners = new ArrayList<>(
115                        Collections.singleton(new AprLifecycleListener()));
116
117        private List<TomcatContextCustomizer> tomcatContextCustomizers = new ArrayList<>();
118
119        private List<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<>();
120
121        private List<Connector> additionalTomcatConnectors = new ArrayList<>();
122
123        private ResourceLoader resourceLoader;
124
125        private String protocol = DEFAULT_PROTOCOL;
126
127        private Set<String> tldSkipPatterns = new LinkedHashSet<>(TldSkipPatterns.DEFAULT);
128
129        private Charset uriEncoding = DEFAULT_CHARSET;
130
131        private int backgroundProcessorDelay;
132
133        /**
134         * Create a new {@link TomcatServletWebServerFactory} instance.
135         */
136        public TomcatServletWebServerFactory() {
137        }
138
139        /**
140         * Create a new {@link TomcatServletWebServerFactory} that listens for requests using
141         * the specified port.
142         * @param port the port to listen on
143         */
144        public TomcatServletWebServerFactory(int port) {
145                super(port);
146        }
147
148        /**
149         * Create a new {@link TomcatServletWebServerFactory} with the specified context path
150         * and port.
151         * @param contextPath the root context path
152         * @param port the port to listen on
153         */
154        public TomcatServletWebServerFactory(String contextPath, int port) {
155                super(contextPath, port);
156        }
157
158        @Override
159        public WebServer getWebServer(ServletContextInitializer... initializers) {
160                Tomcat tomcat = new Tomcat();
161                File baseDir = (this.baseDirectory != null) ? this.baseDirectory
162                                : createTempDir("tomcat");
163                tomcat.setBaseDir(baseDir.getAbsolutePath());
164                Connector connector = new Connector(this.protocol);
165                tomcat.getService().addConnector(connector);
166                customizeConnector(connector);
167                tomcat.setConnector(connector);
168                tomcat.getHost().setAutoDeploy(false);
169                configureEngine(tomcat.getEngine());
170                for (Connector additionalConnector : this.additionalTomcatConnectors) {
171                        tomcat.getService().addConnector(additionalConnector);
172                }
173                prepareContext(tomcat.getHost(), initializers);
174                return getTomcatWebServer(tomcat);
175        }
176
177        private void configureEngine(Engine engine) {
178                engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay);
179                for (Valve valve : this.engineValves) {
180                        engine.getPipeline().addValve(valve);
181                }
182        }
183
184        protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
185                File documentRoot = getValidDocumentRoot();
186                TomcatEmbeddedContext context = new TomcatEmbeddedContext();
187                if (documentRoot != null) {
188                        context.setResources(new LoaderHidingResourceRoot(context));
189                }
190                context.setName(getContextPath());
191                context.setDisplayName(getDisplayName());
192                context.setPath(getContextPath());
193                File docBase = (documentRoot != null) ? documentRoot
194                                : createTempDir("tomcat-docbase");
195                context.setDocBase(docBase.getAbsolutePath());
196                context.addLifecycleListener(new FixContextListener());
197                context.setParentClassLoader(
198                                (this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
199                                                : ClassUtils.getDefaultClassLoader());
200                resetDefaultLocaleMapping(context);
201                addLocaleMappings(context);
202                context.setUseRelativeRedirects(false);
203                configureTldSkipPatterns(context);
204                WebappLoader loader = new WebappLoader(context.getParentClassLoader());
205                loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
206                loader.setDelegate(true);
207                context.setLoader(loader);
208                if (isRegisterDefaultServlet()) {
209                        addDefaultServlet(context);
210                }
211                if (shouldRegisterJspServlet()) {
212                        addJspServlet(context);
213                        addJasperInitializer(context);
214                }
215                context.addLifecycleListener(new StaticResourceConfigurer(context));
216                ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
217                host.addChild(context);
218                configureContext(context, initializersToUse);
219                postProcessContext(context);
220        }
221
222        /**
223         * Override Tomcat's default locale mappings to align with other servers. See
224         * {@code org.apache.catalina.util.CharsetMapperDefault.properties}.
225         * @param context the context to reset
226         */
227        private void resetDefaultLocaleMapping(TomcatEmbeddedContext context) {
228                context.addLocaleEncodingMappingParameter(Locale.ENGLISH.toString(),
229                                DEFAULT_CHARSET.displayName());
230                context.addLocaleEncodingMappingParameter(Locale.FRENCH.toString(),
231                                DEFAULT_CHARSET.displayName());
232        }
233
234        private void addLocaleMappings(TomcatEmbeddedContext context) {
235                getLocaleCharsetMappings()
236                                .forEach((locale, charset) -> context.addLocaleEncodingMappingParameter(
237                                                locale.toString(), charset.toString()));
238        }
239
240        private void configureTldSkipPatterns(TomcatEmbeddedContext context) {
241                StandardJarScanFilter filter = new StandardJarScanFilter();
242                filter.setTldSkip(
243                                StringUtils.collectionToCommaDelimitedString(this.tldSkipPatterns));
244                context.getJarScanner().setJarScanFilter(filter);
245        }
246
247        private void addDefaultServlet(Context context) {
248                Wrapper defaultServlet = context.createWrapper();
249                defaultServlet.setName("default");
250                defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
251                defaultServlet.addInitParameter("debug", "0");
252                defaultServlet.addInitParameter("listings", "false");
253                defaultServlet.setLoadOnStartup(1);
254                // Otherwise the default location of a Spring DispatcherServlet cannot be set
255                defaultServlet.setOverridable(true);
256                context.addChild(defaultServlet);
257                context.addServletMappingDecoded("/", "default");
258        }
259
260        private void addJspServlet(Context context) {
261                Wrapper jspServlet = context.createWrapper();
262                jspServlet.setName("jsp");
263                jspServlet.setServletClass(getJsp().getClassName());
264                jspServlet.addInitParameter("fork", "false");
265                getJsp().getInitParameters().forEach(jspServlet::addInitParameter);
266                jspServlet.setLoadOnStartup(3);
267                context.addChild(jspServlet);
268                context.addServletMappingDecoded("*.jsp", "jsp");
269                context.addServletMappingDecoded("*.jspx", "jsp");
270        }
271
272        private void addJasperInitializer(TomcatEmbeddedContext context) {
273                try {
274                        ServletContainerInitializer initializer = (ServletContainerInitializer) ClassUtils
275                                        .forName("org.apache.jasper.servlet.JasperInitializer", null)
276                                        .newInstance();
277                        context.addServletContainerInitializer(initializer, null);
278                }
279                catch (Exception ex) {
280                        // Probably not Tomcat 8
281                }
282        }
283
284        // Needs to be protected so it can be used by subclasses
285        protected void customizeConnector(Connector connector) {
286                int port = (getPort() >= 0) ? getPort() : 0;
287                connector.setPort(port);
288                if (StringUtils.hasText(this.getServerHeader())) {
289                        connector.setAttribute("server", this.getServerHeader());
290                }
291                if (connector.getProtocolHandler() instanceof AbstractProtocol) {
292                        customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler());
293                }
294                if (getUriEncoding() != null) {
295                        connector.setURIEncoding(getUriEncoding().name());
296                }
297                // Don't bind to the socket prematurely if ApplicationContext is slow to start
298                connector.setProperty("bindOnInit", "false");
299                if (getSsl() != null && getSsl().isEnabled()) {
300                        customizeSsl(connector);
301                }
302                TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(
303                                getCompression());
304                compression.customize(connector);
305                for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) {
306                        customizer.customize(connector);
307                }
308        }
309
310        private void customizeProtocol(AbstractProtocol<?> protocol) {
311                if (getAddress() != null) {
312                        protocol.setAddress(getAddress());
313                }
314        }
315
316        private void customizeSsl(Connector connector) {
317                new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector);
318                if (getHttp2() != null && getHttp2().isEnabled()) {
319                        connector.addUpgradeProtocol(new Http2Protocol());
320                }
321        }
322
323        /**
324         * Configure the Tomcat {@link Context}.
325         * @param context the Tomcat context
326         * @param initializers initializers to apply
327         */
328        protected void configureContext(Context context,
329                        ServletContextInitializer[] initializers) {
330                TomcatStarter starter = new TomcatStarter(initializers);
331                if (context instanceof TomcatEmbeddedContext) {
332                        TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
333                        embeddedContext.setStarter(starter);
334                        embeddedContext.setFailCtxIfServletStartFails(true);
335                }
336                context.addServletContainerInitializer(starter, NO_CLASSES);
337                for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
338                        context.addLifecycleListener(lifecycleListener);
339                }
340                for (Valve valve : this.contextValves) {
341                        context.getPipeline().addValve(valve);
342                }
343                for (ErrorPage errorPage : getErrorPages()) {
344                        new TomcatErrorPage(errorPage).addToContext(context);
345                }
346                for (MimeMappings.Mapping mapping : getMimeMappings()) {
347                        context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
348                }
349                configureSession(context);
350                for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
351                        customizer.customize(context);
352                }
353        }
354
355        private void configureSession(Context context) {
356                long sessionTimeout = getSessionTimeoutInMinutes();
357                context.setSessionTimeout((int) sessionTimeout);
358                Boolean httpOnly = getSession().getCookie().getHttpOnly();
359                if (httpOnly != null) {
360                        context.setUseHttpOnly(httpOnly);
361                }
362                if (getSession().isPersistent()) {
363                        Manager manager = context.getManager();
364                        if (manager == null) {
365                                manager = new StandardManager();
366                                context.setManager(manager);
367                        }
368                        configurePersistSession(manager);
369                }
370                else {
371                        context.addLifecycleListener(new DisablePersistSessionListener());
372                }
373        }
374
375        private void configurePersistSession(Manager manager) {
376                Assert.state(manager instanceof StandardManager,
377                                () -> "Unable to persist HTTP session state using manager type "
378                                                + manager.getClass().getName());
379                File dir = getValidSessionStoreDir();
380                File file = new File(dir, "SESSIONS.ser");
381                ((StandardManager) manager).setPathname(file.getAbsolutePath());
382        }
383
384        private long getSessionTimeoutInMinutes() {
385                Duration sessionTimeout = getSession().getTimeout();
386                if (isZeroOrLess(sessionTimeout)) {
387                        return 0;
388                }
389                return Math.max(sessionTimeout.toMinutes(), 1);
390        }
391
392        private boolean isZeroOrLess(Duration sessionTimeout) {
393                return sessionTimeout == null || sessionTimeout.isNegative()
394                                || sessionTimeout.isZero();
395        }
396
397        /**
398         * Post process the Tomcat {@link Context} before it's used with the Tomcat Server.
399         * Subclasses can override this method to apply additional processing to the
400         * {@link Context}.
401         * @param context the Tomcat {@link Context}
402         */
403        protected void postProcessContext(Context context) {
404        }
405
406        /**
407         * Factory method called to create the {@link TomcatWebServer}. Subclasses can
408         * override this method to return a different {@link TomcatWebServer} or apply
409         * additional processing to the Tomcat server.
410         * @param tomcat the Tomcat server.
411         * @return a new {@link TomcatWebServer} instance
412         */
413        protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
414                return new TomcatWebServer(tomcat, getPort() >= 0);
415        }
416
417        @Override
418        public void setResourceLoader(ResourceLoader resourceLoader) {
419                this.resourceLoader = resourceLoader;
420        }
421
422        @Override
423        public void setBaseDirectory(File baseDirectory) {
424                this.baseDirectory = baseDirectory;
425        }
426
427        /**
428         * Returns a mutable set of the patterns that match jars to ignore for TLD scanning.
429         * @return the list of jars to ignore for TLD scanning
430         */
431        public Set<String> getTldSkipPatterns() {
432                return this.tldSkipPatterns;
433        }
434
435        /**
436         * Set the patterns that match jars to ignore for TLD scanning. See Tomcat's
437         * catalina.properties for typical values. Defaults to a list drawn from that source.
438         * @param patterns the jar patterns to skip when scanning for TLDs etc
439         */
440        public void setTldSkipPatterns(Collection<String> patterns) {
441                Assert.notNull(patterns, "Patterns must not be null");
442                this.tldSkipPatterns = new LinkedHashSet<>(patterns);
443        }
444
445        /**
446         * Add patterns that match jars to ignore for TLD scanning. See Tomcat's
447         * catalina.properties for typical values.
448         * @param patterns the additional jar patterns to skip when scanning for TLDs etc
449         */
450        public void addTldSkipPatterns(String... patterns) {
451                Assert.notNull(patterns, "Patterns must not be null");
452                this.tldSkipPatterns.addAll(Arrays.asList(patterns));
453        }
454
455        /**
456         * The Tomcat protocol to use when create the {@link Connector}.
457         * @param protocol the protocol
458         * @see Connector#Connector(String)
459         */
460        public void setProtocol(String protocol) {
461                Assert.hasLength(protocol, "Protocol must not be empty");
462                this.protocol = protocol;
463        }
464
465        /**
466         * Set {@link Valve}s that should be applied to the Tomcat {@link Engine}. Calling
467         * this method will replace any existing valves.
468         * @param engineValves the valves to set
469         */
470        public void setEngineValves(Collection<? extends Valve> engineValves) {
471                Assert.notNull(engineValves, "Valves must not be null");
472                this.engineValves = new ArrayList<>(engineValves);
473        }
474
475        /**
476         * Returns a mutable collection of the {@link Valve}s that will be applied to the
477         * Tomcat {@link Engine}.
478         * @return the engine valves that will be applied
479         */
480        public Collection<Valve> getEngineValves() {
481                return this.engineValves;
482        }
483
484        @Override
485        public void addEngineValves(Valve... engineValves) {
486                Assert.notNull(engineValves, "Valves must not be null");
487                this.engineValves.addAll(Arrays.asList(engineValves));
488        }
489
490        /**
491         * Set {@link Valve}s that should be applied to the Tomcat {@link Context}. Calling
492         * this method will replace any existing valves.
493         * @param contextValves the valves to set
494         */
495        public void setContextValves(Collection<? extends Valve> contextValves) {
496                Assert.notNull(contextValves, "Valves must not be null");
497                this.contextValves = new ArrayList<>(contextValves);
498        }
499
500        /**
501         * Returns a mutable collection of the {@link Valve}s that will be applied to the
502         * Tomcat {@link Context}.
503         * @return the context valves that will be applied
504         * @see #getEngineValves()
505         */
506        public Collection<Valve> getContextValves() {
507                return this.contextValves;
508        }
509
510        /**
511         * Add {@link Valve}s that should be applied to the Tomcat {@link Context}.
512         * @param contextValves the valves to add
513         */
514        public void addContextValves(Valve... contextValves) {
515                Assert.notNull(contextValves, "Valves must not be null");
516                this.contextValves.addAll(Arrays.asList(contextValves));
517        }
518
519        /**
520         * Set {@link LifecycleListener}s that should be applied to the Tomcat
521         * {@link Context}. Calling this method will replace any existing listeners.
522         * @param contextLifecycleListeners the listeners to set
523         */
524        public void setContextLifecycleListeners(
525                        Collection<? extends LifecycleListener> contextLifecycleListeners) {
526                Assert.notNull(contextLifecycleListeners,
527                                "ContextLifecycleListeners must not be null");
528                this.contextLifecycleListeners = new ArrayList<>(contextLifecycleListeners);
529        }
530
531        /**
532         * Returns a mutable collection of the {@link LifecycleListener}s that will be applied
533         * to the Tomcat {@link Context}.
534         * @return the context lifecycle listeners that will be applied
535         */
536        public Collection<LifecycleListener> getContextLifecycleListeners() {
537                return this.contextLifecycleListeners;
538        }
539
540        /**
541         * Add {@link LifecycleListener}s that should be added to the Tomcat {@link Context}.
542         * @param contextLifecycleListeners the listeners to add
543         */
544        public void addContextLifecycleListeners(
545                        LifecycleListener... contextLifecycleListeners) {
546                Assert.notNull(contextLifecycleListeners,
547                                "ContextLifecycleListeners must not be null");
548                this.contextLifecycleListeners.addAll(Arrays.asList(contextLifecycleListeners));
549        }
550
551        /**
552         * Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat
553         * {@link Context}. Calling this method will replace any existing customizers.
554         * @param tomcatContextCustomizers the customizers to set
555         */
556        public void setTomcatContextCustomizers(
557                        Collection<? extends TomcatContextCustomizer> tomcatContextCustomizers) {
558                Assert.notNull(tomcatContextCustomizers,
559                                "TomcatContextCustomizers must not be null");
560                this.tomcatContextCustomizers = new ArrayList<>(tomcatContextCustomizers);
561        }
562
563        /**
564         * Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be
565         * applied to the Tomcat {@link Context}.
566         * @return the listeners that will be applied
567         */
568        public Collection<TomcatContextCustomizer> getTomcatContextCustomizers() {
569                return this.tomcatContextCustomizers;
570        }
571
572        @Override
573        public void addContextCustomizers(
574                        TomcatContextCustomizer... tomcatContextCustomizers) {
575                Assert.notNull(tomcatContextCustomizers,
576                                "TomcatContextCustomizers must not be null");
577                this.tomcatContextCustomizers.addAll(Arrays.asList(tomcatContextCustomizers));
578        }
579
580        /**
581         * Set {@link TomcatConnectorCustomizer}s that should be applied to the Tomcat
582         * {@link Connector}. Calling this method will replace any existing customizers.
583         * @param tomcatConnectorCustomizers the customizers to set
584         */
585        public void setTomcatConnectorCustomizers(
586                        Collection<? extends TomcatConnectorCustomizer> tomcatConnectorCustomizers) {
587                Assert.notNull(tomcatConnectorCustomizers,
588                                "TomcatConnectorCustomizers must not be null");
589                this.tomcatConnectorCustomizers = new ArrayList<>(tomcatConnectorCustomizers);
590        }
591
592        @Override
593        public void addConnectorCustomizers(
594                        TomcatConnectorCustomizer... tomcatConnectorCustomizers) {
595                Assert.notNull(tomcatConnectorCustomizers,
596                                "TomcatConnectorCustomizers must not be null");
597                this.tomcatConnectorCustomizers.addAll(Arrays.asList(tomcatConnectorCustomizers));
598        }
599
600        /**
601         * Returns a mutable collection of the {@link TomcatConnectorCustomizer}s that will be
602         * applied to the Tomcat {@link Connector}.
603         * @return the customizers that will be applied
604         */
605        public Collection<TomcatConnectorCustomizer> getTomcatConnectorCustomizers() {
606                return this.tomcatConnectorCustomizers;
607        }
608
609        /**
610         * Add {@link Connector}s in addition to the default connector, e.g. for SSL or AJP
611         * @param connectors the connectors to add
612         */
613        public void addAdditionalTomcatConnectors(Connector... connectors) {
614                Assert.notNull(connectors, "Connectors must not be null");
615                this.additionalTomcatConnectors.addAll(Arrays.asList(connectors));
616        }
617
618        /**
619         * Returns a mutable collection of the {@link Connector}s that will be added to the
620         * Tomcat.
621         * @return the additionalTomcatConnectors
622         */
623        public List<Connector> getAdditionalTomcatConnectors() {
624                return this.additionalTomcatConnectors;
625        }
626
627        @Override
628        public void setUriEncoding(Charset uriEncoding) {
629                this.uriEncoding = uriEncoding;
630        }
631
632        /**
633         * Returns the character encoding to use for URL decoding.
634         * @return the URI encoding
635         */
636        public Charset getUriEncoding() {
637                return this.uriEncoding;
638        }
639
640        @Override
641        public void setBackgroundProcessorDelay(int delay) {
642                this.backgroundProcessorDelay = delay;
643        }
644
645        /**
646         * {@link LifecycleListener} to disable persistence in the {@link StandardManager}. A
647         * {@link LifecycleListener} is used so not to interfere with Tomcat's default manager
648         * creation logic.
649         */
650        private static class DisablePersistSessionListener implements LifecycleListener {
651
652                @Override
653                public void lifecycleEvent(LifecycleEvent event) {
654                        if (event.getType().equals(Lifecycle.START_EVENT)) {
655                                Context context = (Context) event.getLifecycle();
656                                Manager manager = context.getManager();
657                                if (manager != null && manager instanceof StandardManager) {
658                                        ((StandardManager) manager).setPathname(null);
659                                }
660                        }
661                }
662
663        }
664
665        private final class StaticResourceConfigurer implements LifecycleListener {
666
667                private final Context context;
668
669                private StaticResourceConfigurer(Context context) {
670                        this.context = context;
671                }
672
673                @Override
674                public void lifecycleEvent(LifecycleEvent event) {
675                        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
676                                addResourceJars(getUrlsOfJarsWithMetaInfResources());
677                        }
678                }
679
680                private void addResourceJars(List<URL> resourceJarUrls) {
681                        for (URL url : resourceJarUrls) {
682                                String path = url.getPath();
683                                if (path.endsWith(".jar") || path.endsWith(".jar!/")) {
684                                        String jar = url.toString();
685                                        if (!jar.startsWith("jar:")) {
686                                                // A jar file in the file system. Convert to Jar URL.
687                                                jar = "jar:" + jar + "!/";
688                                        }
689                                        addResourceSet(jar);
690                                }
691                                else {
692                                        addResourceSet(url.toString());
693                                }
694                        }
695                }
696
697                private void addResourceSet(String resource) {
698                        try {
699                                if (isInsideNestedJar(resource)) {
700                                        // It's a nested jar but we now don't want the suffix because Tomcat
701                                        // is going to try and locate it as a root URL (not the resource
702                                        // inside it)
703                                        resource = resource.substring(0, resource.length() - 2);
704                                }
705                                URL url = new URL(resource);
706                                String path = "/META-INF/resources";
707                                this.context.getResources().createWebResourceSet(
708                                                ResourceSetType.RESOURCE_JAR, "/", url, path);
709                        }
710                        catch (Exception ex) {
711                                // Ignore (probably not a directory)
712                        }
713                }
714
715                private boolean isInsideNestedJar(String dir) {
716                        return dir.indexOf("!/") < dir.lastIndexOf("!/");
717                }
718
719        }
720
721        private static final class LoaderHidingResourceRoot extends StandardRoot {
722
723                private LoaderHidingResourceRoot(TomcatEmbeddedContext context) {
724                        super(context);
725                }
726
727                @Override
728                protected WebResourceSet createMainResourceSet() {
729                        return new LoaderHidingWebResourceSet(super.createMainResourceSet());
730                }
731
732        }
733
734        private static final class LoaderHidingWebResourceSet extends AbstractResourceSet {
735
736                private final WebResourceSet delegate;
737
738                private final Method initInternal;
739
740                private LoaderHidingWebResourceSet(WebResourceSet delegate) {
741                        this.delegate = delegate;
742                        try {
743                                this.initInternal = LifecycleBase.class.getDeclaredMethod("initInternal");
744                                this.initInternal.setAccessible(true);
745                        }
746                        catch (Exception ex) {
747                                throw new IllegalStateException(ex);
748                        }
749                }
750
751                @Override
752                public WebResource getResource(String path) {
753                        if (path.startsWith("/org/springframework/boot")) {
754                                return new EmptyResource(getRoot(), path);
755                        }
756                        return this.delegate.getResource(path);
757                }
758
759                @Override
760                public String[] list(String path) {
761                        return this.delegate.list(path);
762                }
763
764                @Override
765                public Set<String> listWebAppPaths(String path) {
766                        return this.delegate.listWebAppPaths(path);
767                }
768
769                @Override
770                public boolean mkdir(String path) {
771                        return this.delegate.mkdir(path);
772                }
773
774                @Override
775                public boolean write(String path, InputStream is, boolean overwrite) {
776                        return this.delegate.write(path, is, overwrite);
777                }
778
779                @Override
780                public URL getBaseUrl() {
781                        return this.delegate.getBaseUrl();
782                }
783
784                @Override
785                public void setReadOnly(boolean readOnly) {
786                        this.delegate.setReadOnly(readOnly);
787                }
788
789                @Override
790                public boolean isReadOnly() {
791                        return this.delegate.isReadOnly();
792                }
793
794                @Override
795                public void gc() {
796                        this.delegate.gc();
797                }
798
799                @Override
800                protected void initInternal() throws LifecycleException {
801                        if (this.delegate instanceof LifecycleBase) {
802                                try {
803                                        ReflectionUtils.invokeMethod(this.initInternal, this.delegate);
804                                }
805                                catch (Exception ex) {
806                                        throw new LifecycleException(ex);
807                                }
808                        }
809                }
810
811        }
812
813}