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.jetty;
018
019import java.net.BindException;
020import java.util.Arrays;
021import java.util.List;
022import java.util.stream.Collectors;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026import org.eclipse.jetty.server.Connector;
027import org.eclipse.jetty.server.Handler;
028import org.eclipse.jetty.server.NetworkConnector;
029import org.eclipse.jetty.server.Server;
030import org.eclipse.jetty.server.handler.ContextHandler;
031import org.eclipse.jetty.server.handler.HandlerCollection;
032import org.eclipse.jetty.server.handler.HandlerWrapper;
033import org.eclipse.jetty.util.component.AbstractLifeCycle;
034
035import org.springframework.boot.web.server.PortInUseException;
036import org.springframework.boot.web.server.WebServer;
037import org.springframework.boot.web.server.WebServerException;
038import org.springframework.util.Assert;
039import org.springframework.util.ReflectionUtils;
040import org.springframework.util.StringUtils;
041
042/**
043 * {@link WebServer} that can be used to control a Jetty web server.
044 *
045 * @author Phillip Webb
046 * @author Dave Syer
047 * @author David Liu
048 * @author EddĂș MelĂ©ndez
049 * @author Brian Clozel
050 * @author Kristine Jetzke
051 * @since 2.0.0
052 * @see JettyReactiveWebServerFactory
053 */
054public class JettyWebServer implements WebServer {
055
056        private static final Log logger = LogFactory.getLog(JettyWebServer.class);
057
058        private final Object monitor = new Object();
059
060        private final Server server;
061
062        private final boolean autoStart;
063
064        private Connector[] connectors;
065
066        private volatile boolean started;
067
068        /**
069         * Create a new {@link JettyWebServer} instance.
070         * @param server the underlying Jetty server
071         */
072        public JettyWebServer(Server server) {
073                this(server, true);
074        }
075
076        /**
077         * Create a new {@link JettyWebServer} instance.
078         * @param server the underlying Jetty server
079         * @param autoStart if auto-starting the server
080         */
081        public JettyWebServer(Server server, boolean autoStart) {
082                this.autoStart = autoStart;
083                Assert.notNull(server, "Jetty Server must not be null");
084                this.server = server;
085                initialize();
086        }
087
088        private void initialize() {
089                synchronized (this.monitor) {
090                        try {
091                                // Cache the connectors and then remove them to prevent requests being
092                                // handled before the application context is ready.
093                                this.connectors = this.server.getConnectors();
094                                this.server.addBean(new AbstractLifeCycle() {
095
096                                        @Override
097                                        protected void doStart() throws Exception {
098                                                for (Connector connector : JettyWebServer.this.connectors) {
099                                                        Assert.state(connector.isStopped(), () -> "Connector "
100                                                                        + connector + " has been started prematurely");
101                                                }
102                                                JettyWebServer.this.server.setConnectors(null);
103                                        }
104
105                                });
106                                // Start the server so that the ServletContext is available
107                                this.server.start();
108                                this.server.setStopAtShutdown(false);
109                        }
110                        catch (Throwable ex) {
111                                // Ensure process isn't left running
112                                stopSilently();
113                                throw new WebServerException("Unable to start embedded Jetty web server",
114                                                ex);
115                        }
116                }
117        }
118
119        private void stopSilently() {
120                try {
121                        this.server.stop();
122                }
123                catch (Exception ex) {
124                        // Ignore
125                }
126        }
127
128        @Override
129        public void start() throws WebServerException {
130                synchronized (this.monitor) {
131                        if (this.started) {
132                                return;
133                        }
134                        this.server.setConnectors(this.connectors);
135                        if (!this.autoStart) {
136                                return;
137                        }
138                        try {
139                                this.server.start();
140                                for (Handler handler : this.server.getHandlers()) {
141                                        handleDeferredInitialize(handler);
142                                }
143                                Connector[] connectors = this.server.getConnectors();
144                                for (Connector connector : connectors) {
145                                        try {
146                                                connector.start();
147                                        }
148                                        catch (BindException ex) {
149                                                if (connector instanceof NetworkConnector) {
150                                                        throw new PortInUseException(
151                                                                        ((NetworkConnector) connector).getPort());
152                                                }
153                                                throw ex;
154                                        }
155                                }
156                                this.started = true;
157                                logger.info("Jetty started on port(s) " + getActualPortsDescription()
158                                                + " with context path '" + getContextPath() + "'");
159                        }
160                        catch (WebServerException ex) {
161                                stopSilently();
162                                throw ex;
163                        }
164                        catch (Exception ex) {
165                                stopSilently();
166                                throw new WebServerException("Unable to start embedded Jetty server", ex);
167                        }
168                }
169        }
170
171        private String getActualPortsDescription() {
172                StringBuilder ports = new StringBuilder();
173                for (Connector connector : this.server.getConnectors()) {
174                        if (ports.length() != 0) {
175                                ports.append(", ");
176                        }
177                        ports.append(getLocalPort(connector)).append(getProtocols(connector));
178                }
179                return ports.toString();
180        }
181
182        private Integer getLocalPort(Connector connector) {
183                try {
184                        // Jetty 9 internals are different, but the method name is the same
185                        return (Integer) ReflectionUtils.invokeMethod(
186                                        ReflectionUtils.findMethod(connector.getClass(), "getLocalPort"),
187                                        connector);
188                }
189                catch (Exception ex) {
190                        logger.info("could not determine port ( " + ex.getMessage() + ")");
191                        return 0;
192                }
193        }
194
195        private String getProtocols(Connector connector) {
196                List<String> protocols = connector.getProtocols();
197                return " (" + StringUtils.collectionToDelimitedString(protocols, ", ") + ")";
198        }
199
200        private String getContextPath() {
201                return Arrays.stream(this.server.getHandlers())
202                                .filter(ContextHandler.class::isInstance).map(ContextHandler.class::cast)
203                                .map(ContextHandler::getContextPath).collect(Collectors.joining(" "));
204        }
205
206        private void handleDeferredInitialize(Handler... handlers) throws Exception {
207                for (Handler handler : handlers) {
208                        if (handler instanceof JettyEmbeddedWebAppContext) {
209                                ((JettyEmbeddedWebAppContext) handler).deferredInitialize();
210                        }
211                        else if (handler instanceof HandlerWrapper) {
212                                handleDeferredInitialize(((HandlerWrapper) handler).getHandler());
213                        }
214                        else if (handler instanceof HandlerCollection) {
215                                handleDeferredInitialize(((HandlerCollection) handler).getHandlers());
216                        }
217                }
218        }
219
220        @Override
221        public void stop() {
222                synchronized (this.monitor) {
223                        this.started = false;
224                        try {
225                                this.server.stop();
226                        }
227                        catch (InterruptedException ex) {
228                                Thread.currentThread().interrupt();
229                        }
230                        catch (Exception ex) {
231                                throw new WebServerException("Unable to stop embedded Jetty server", ex);
232                        }
233                }
234        }
235
236        @Override
237        public int getPort() {
238                Connector[] connectors = this.server.getConnectors();
239                for (Connector connector : connectors) {
240                        // Probably only one...
241                        return getLocalPort(connector);
242                }
243                return 0;
244        }
245
246        /**
247         * Returns access to the underlying Jetty Server.
248         * @return the Jetty server
249         */
250        public Server getServer() {
251                return this.server;
252        }
253
254}