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.nio.charset.Charset;
021import java.nio.charset.StandardCharsets;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027
028import org.apache.catalina.Context;
029import org.apache.catalina.Engine;
030import org.apache.catalina.Host;
031import org.apache.catalina.LifecycleListener;
032import org.apache.catalina.Valve;
033import org.apache.catalina.connector.Connector;
034import org.apache.catalina.core.AprLifecycleListener;
035import org.apache.catalina.loader.WebappLoader;
036import org.apache.catalina.startup.Tomcat;
037import org.apache.coyote.AbstractProtocol;
038import org.apache.coyote.http2.Http2Protocol;
039import org.apache.tomcat.util.scan.StandardJarScanFilter;
040
041import org.springframework.boot.web.reactive.server.AbstractReactiveWebServerFactory;
042import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
043import org.springframework.boot.web.server.WebServer;
044import org.springframework.http.server.reactive.HttpHandler;
045import org.springframework.http.server.reactive.TomcatHttpHandlerAdapter;
046import org.springframework.util.Assert;
047import org.springframework.util.ClassUtils;
048import org.springframework.util.StringUtils;
049
050/**
051 * {@link ReactiveWebServerFactory} that can be used to create a {@link TomcatWebServer}.
052 *
053 * @author Brian Clozel
054 * @since 2.0.0
055 */
056public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory
057                implements ConfigurableTomcatWebServerFactory {
058
059        private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
060
061        /**
062         * The class name of default protocol used.
063         */
064        public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";
065
066        private File baseDirectory;
067
068        private List<Valve> engineValves = new ArrayList<>();
069
070        private List<LifecycleListener> contextLifecycleListeners = new ArrayList<>(
071                        Collections.singleton(new AprLifecycleListener()));
072
073        private List<TomcatContextCustomizer> tomcatContextCustomizers = new ArrayList<>();
074
075        private List<TomcatConnectorCustomizer> tomcatConnectorCustomizers = new ArrayList<>();
076
077        private String protocol = DEFAULT_PROTOCOL;
078
079        private Charset uriEncoding = DEFAULT_CHARSET;
080
081        private int backgroundProcessorDelay;
082
083        /**
084         * Create a new {@link TomcatServletWebServerFactory} instance.
085         */
086        public TomcatReactiveWebServerFactory() {
087        }
088
089        /**
090         * Create a new {@link TomcatServletWebServerFactory} that listens for requests using
091         * the specified port.
092         * @param port the port to listen on
093         */
094        public TomcatReactiveWebServerFactory(int port) {
095                super(port);
096        }
097
098        @Override
099        public WebServer getWebServer(HttpHandler httpHandler) {
100                Tomcat tomcat = new Tomcat();
101                File baseDir = (this.baseDirectory != null) ? this.baseDirectory
102                                : createTempDir("tomcat");
103                tomcat.setBaseDir(baseDir.getAbsolutePath());
104                Connector connector = new Connector(this.protocol);
105                tomcat.getService().addConnector(connector);
106                customizeConnector(connector);
107                tomcat.setConnector(connector);
108                tomcat.getHost().setAutoDeploy(false);
109                configureEngine(tomcat.getEngine());
110                TomcatHttpHandlerAdapter servlet = new TomcatHttpHandlerAdapter(httpHandler);
111                prepareContext(tomcat.getHost(), servlet);
112                return new TomcatWebServer(tomcat, getPort() >= 0);
113        }
114
115        private void configureEngine(Engine engine) {
116                engine.setBackgroundProcessorDelay(this.backgroundProcessorDelay);
117                for (Valve valve : this.engineValves) {
118                        engine.getPipeline().addValve(valve);
119                }
120        }
121
122        protected void prepareContext(Host host, TomcatHttpHandlerAdapter servlet) {
123                File docBase = createTempDir("tomcat-docbase");
124                TomcatEmbeddedContext context = new TomcatEmbeddedContext();
125                context.setPath("");
126                context.setDocBase(docBase.getAbsolutePath());
127                context.addLifecycleListener(new Tomcat.FixContextListener());
128                context.setParentClassLoader(ClassUtils.getDefaultClassLoader());
129                skipAllTldScanning(context);
130                WebappLoader loader = new WebappLoader(context.getParentClassLoader());
131                loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
132                loader.setDelegate(true);
133                context.setLoader(loader);
134                Tomcat.addServlet(context, "httpHandlerServlet", servlet).setAsyncSupported(true);
135                context.addServletMappingDecoded("/", "httpHandlerServlet");
136                host.addChild(context);
137                configureContext(context);
138        }
139
140        private void skipAllTldScanning(TomcatEmbeddedContext context) {
141                StandardJarScanFilter filter = new StandardJarScanFilter();
142                filter.setTldSkip("*.jar");
143                context.getJarScanner().setJarScanFilter(filter);
144        }
145
146        /**
147         * Configure the Tomcat {@link Context}.
148         * @param context the Tomcat context
149         */
150        protected void configureContext(Context context) {
151                this.contextLifecycleListeners.forEach(context::addLifecycleListener);
152                this.tomcatContextCustomizers
153                                .forEach((customizer) -> customizer.customize(context));
154        }
155
156        protected void customizeConnector(Connector connector) {
157                int port = (getPort() >= 0) ? getPort() : 0;
158                connector.setPort(port);
159                if (StringUtils.hasText(this.getServerHeader())) {
160                        connector.setAttribute("server", this.getServerHeader());
161                }
162                if (connector.getProtocolHandler() instanceof AbstractProtocol) {
163                        customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler());
164                }
165                if (getUriEncoding() != null) {
166                        connector.setURIEncoding(getUriEncoding().name());
167                }
168                // Don't bind to the socket prematurely if ApplicationContext is slow to start
169                connector.setProperty("bindOnInit", "false");
170                if (getSsl() != null && getSsl().isEnabled()) {
171                        customizeSsl(connector);
172                }
173                TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(
174                                getCompression());
175                compression.customize(connector);
176                for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) {
177                        customizer.customize(connector);
178                }
179        }
180
181        private void customizeProtocol(AbstractProtocol<?> protocol) {
182                if (getAddress() != null) {
183                        protocol.setAddress(getAddress());
184                }
185        }
186
187        private void customizeSsl(Connector connector) {
188                new SslConnectorCustomizer(getSsl(), getSslStoreProvider()).customize(connector);
189                if (getHttp2() != null && getHttp2().isEnabled()) {
190                        connector.addUpgradeProtocol(new Http2Protocol());
191                }
192        }
193
194        @Override
195        public void setBaseDirectory(File baseDirectory) {
196                this.baseDirectory = baseDirectory;
197        }
198
199        @Override
200        public void setBackgroundProcessorDelay(int delay) {
201                this.backgroundProcessorDelay = delay;
202        }
203
204        /**
205         * Set {@link TomcatContextCustomizer}s that should be applied to the Tomcat
206         * {@link Context}. Calling this method will replace any existing customizers.
207         * @param tomcatContextCustomizers the customizers to set
208         */
209        public void setTomcatContextCustomizers(
210                        Collection<? extends TomcatContextCustomizer> tomcatContextCustomizers) {
211                Assert.notNull(tomcatContextCustomizers,
212                                "TomcatContextCustomizers must not be null");
213                this.tomcatContextCustomizers = new ArrayList<>(tomcatContextCustomizers);
214        }
215
216        /**
217         * Returns a mutable collection of the {@link TomcatContextCustomizer}s that will be
218         * applied to the Tomcat {@link Context}.
219         * @return the listeners that will be applied
220         */
221        public Collection<TomcatContextCustomizer> getTomcatContextCustomizers() {
222                return this.tomcatContextCustomizers;
223        }
224
225        /**
226         * Add {@link TomcatContextCustomizer}s that should be added to the Tomcat
227         * {@link Context}.
228         * @param tomcatContextCustomizers the customizers to add
229         */
230        @Override
231        public void addContextCustomizers(
232                        TomcatContextCustomizer... tomcatContextCustomizers) {
233                Assert.notNull(tomcatContextCustomizers,
234                                "TomcatContextCustomizers must not be null");
235                this.tomcatContextCustomizers.addAll(Arrays.asList(tomcatContextCustomizers));
236        }
237
238        /**
239         * Set {@link TomcatConnectorCustomizer}s that should be applied to the Tomcat
240         * {@link Connector}. Calling this method will replace any existing customizers.
241         * @param tomcatConnectorCustomizers the customizers to set
242         */
243        public void setTomcatConnectorCustomizers(
244                        Collection<? extends TomcatConnectorCustomizer> tomcatConnectorCustomizers) {
245                Assert.notNull(tomcatConnectorCustomizers,
246                                "TomcatConnectorCustomizers must not be null");
247                this.tomcatConnectorCustomizers = new ArrayList<>(tomcatConnectorCustomizers);
248        }
249
250        /**
251         * Add {@link TomcatConnectorCustomizer}s that should be added to the Tomcat
252         * {@link Connector}.
253         * @param tomcatConnectorCustomizers the customizers to add
254         */
255        @Override
256        public void addConnectorCustomizers(
257                        TomcatConnectorCustomizer... tomcatConnectorCustomizers) {
258                Assert.notNull(tomcatConnectorCustomizers,
259                                "TomcatConnectorCustomizers must not be null");
260                this.tomcatConnectorCustomizers.addAll(Arrays.asList(tomcatConnectorCustomizers));
261        }
262
263        /**
264         * Returns a mutable collection of the {@link TomcatConnectorCustomizer}s that will be
265         * applied to the Tomcat {@link Connector}.
266         * @return the customizers that will be applied
267         */
268        public Collection<TomcatConnectorCustomizer> getTomcatConnectorCustomizers() {
269                return this.tomcatConnectorCustomizers;
270        }
271
272        @Override
273        public void addEngineValves(Valve... engineValves) {
274                Assert.notNull(engineValves, "Valves must not be null");
275                this.engineValves.addAll(Arrays.asList(engineValves));
276        }
277
278        /**
279         * Returns a mutable collection of the {@link Valve}s that will be applied to the
280         * Tomcat {@link Engine}.
281         * @return the engine valves that will be applied
282         */
283        public List<Valve> getEngineValves() {
284                return this.engineValves;
285        }
286
287        /**
288         * Set the character encoding to use for URL decoding. If not specified 'UTF-8' will
289         * be used.
290         * @param uriEncoding the uri encoding to set
291         */
292        @Override
293        public void setUriEncoding(Charset uriEncoding) {
294                this.uriEncoding = uriEncoding;
295        }
296
297        /**
298         * Returns the character encoding to use for URL decoding.
299         * @return the URI encoding
300         */
301        public Charset getUriEncoding() {
302                return this.uriEncoding;
303        }
304
305        /**
306         * Set {@link LifecycleListener}s that should be applied to the Tomcat
307         * {@link Context}. Calling this method will replace any existing listeners.
308         * @param contextLifecycleListeners the listeners to set
309         */
310        public void setContextLifecycleListeners(
311                        Collection<? extends LifecycleListener> contextLifecycleListeners) {
312                Assert.notNull(contextLifecycleListeners,
313                                "ContextLifecycleListeners must not be null");
314                this.contextLifecycleListeners = new ArrayList<>(contextLifecycleListeners);
315        }
316
317        /**
318         * Returns a mutable collection of the {@link LifecycleListener}s that will be applied
319         * to the Tomcat {@link Context}.
320         * @return the context lifecycle listeners that will be applied
321         */
322        public Collection<LifecycleListener> getContextLifecycleListeners() {
323                return this.contextLifecycleListeners;
324        }
325
326        /**
327         * Add {@link LifecycleListener}s that should be added to the Tomcat {@link Context}.
328         * @param contextLifecycleListeners the listeners to add
329         */
330        public void addContextLifecycleListeners(
331                        LifecycleListener... contextLifecycleListeners) {
332                Assert.notNull(contextLifecycleListeners,
333                                "ContextLifecycleListeners must not be null");
334                this.contextLifecycleListeners.addAll(Arrays.asList(contextLifecycleListeners));
335        }
336
337        /**
338         * Factory method called to create the {@link TomcatWebServer}. Subclasses can
339         * override this method to return a different {@link TomcatWebServer} or apply
340         * additional processing to the Tomcat server.
341         * @param tomcat the Tomcat server.
342         * @return a new {@link TomcatWebServer} instance
343         */
344        protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
345                return new TomcatWebServer(tomcat, getPort() >= 0);
346        }
347
348        /**
349         * The Tomcat protocol to use when create the {@link Connector}.
350         * @param protocol the protocol
351         * @see Connector#Connector(String)
352         */
353        public void setProtocol(String protocol) {
354                Assert.hasLength(protocol, "Protocol must not be empty");
355                this.protocol = protocol;
356        }
357
358}