001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.microservice.jetty; 014 015import static org.apache.juneau.internal.SystemUtils.*; 016 017import java.io.*; 018import java.net.*; 019import java.util.*; 020import java.util.logging.*; 021 022import javax.servlet.*; 023 024import org.apache.juneau.*; 025import org.apache.juneau.config.*; 026import org.apache.juneau.internal.*; 027import org.apache.juneau.microservice.*; 028import org.apache.juneau.rest.*; 029import org.apache.juneau.svl.*; 030import org.apache.juneau.utils.*; 031import org.eclipse.jetty.server.*; 032import org.eclipse.jetty.server.Handler; 033import org.eclipse.jetty.server.handler.*; 034import org.eclipse.jetty.servlet.*; 035 036/** 037 * Entry point for Juneau microservice that implements a REST interface using Jetty on a single port. 038 * 039 * <h5 class='topic'>Jetty Server Details</h5> 040 * 041 * The Jetty server is created by the {@link #createServer()} method and started with the {@link #startServer()} method. 042 * These methods can be overridden to provided customized behavior. 043 * 044 * <h5 class='topic'>Defining REST Resources</h5> 045 * 046 * Top-level REST resources are defined in the <code>jetty.xml</code> file as normal servlets. 047 */ 048public class JettyMicroservice extends Microservice { 049 050 private static volatile JettyMicroservice INSTANCE; 051 052 private static void setInstance(JettyMicroservice m) { 053 synchronized(JettyMicroservice.class) { 054 INSTANCE = m; 055 } 056 } 057 058 /** 059 * Returns the Microservice instance. 060 * <p> 061 * This method only works if there's only one Microservice instance in a JVM. 062 * Otherwise, it's just overwritten by the last instantiated microservice. 063 * 064 * @return The Microservice instance, or <jk>null</jk> if there isn't one. 065 */ 066 public static JettyMicroservice getInstance() { 067 synchronized(JettyMicroservice.class) { 068 return INSTANCE; 069 } 070 } 071 072 /** 073 * Entry-point method. 074 * 075 * @param args Command line arguments. 076 * @throws Exception 077 */ 078 public static void main(String[] args) throws Exception { 079 JettyMicroservice 080 .create() 081 .args(args) 082 .build() 083 .start() 084 .startConsole() 085 .join(); 086 } 087 088 final MessageBundle messages = MessageBundle.create(JettyMicroservice.class); 089 090 //----------------------------------------------------------------------------------------------------------------- 091 // Properties set in constructor 092 //----------------------------------------------------------------------------------------------------------------- 093 private final JettyMicroserviceBuilder builder; 094 final JettyMicroserviceListener listener; 095 private final JettyServerFactory factory; 096 097 //----------------------------------------------------------------------------------------------------------------- 098 // Properties set in constructor 099 //----------------------------------------------------------------------------------------------------------------- 100 volatile Server server; 101 102 /** 103 * Creates a new microservice builder. 104 * 105 * @return A new microservice builder. 106 */ 107 public static JettyMicroserviceBuilder create() { 108 return new JettyMicroserviceBuilder(); 109 } 110 111 /** 112 * Constructor. 113 * 114 * @param builder The constructor arguments. 115 * @throws Exception 116 */ 117 protected JettyMicroservice(JettyMicroserviceBuilder builder) throws Exception { 118 super(builder); 119 setInstance(this); 120 this.builder = builder.copy(); 121 this.listener = builder.listener != null ? builder.listener : new BasicJettyMicroserviceListener(); 122 this.factory = builder.factory != null ? builder.factory : new BasicJettyServerFactory(); 123 } 124 125 //----------------------------------------------------------------------------------------------------------------- 126 // Methods implemented on Microservice API 127 //----------------------------------------------------------------------------------------------------------------- 128 129 @Override /* Microservice */ 130 public JettyMicroservice init() throws Exception { 131 super.init(); 132 return this; 133 } 134 135 @Override /* Microservice */ 136 public JettyMicroservice startConsole() throws Exception { 137 super.startConsole(); 138 return this; 139 } 140 141 @Override /* Microservice */ 142 public JettyMicroservice stopConsole() throws Exception { 143 super.stopConsole(); 144 return this; 145 } 146 147 @Override /* Microservice */ 148 public synchronized JettyMicroservice start() throws Exception { 149 super.start(); 150 createServer(); 151 startServer(); 152 return this; 153 } 154 155 @Override /* Microservice */ 156 public JettyMicroservice join() throws Exception { 157 server.join(); 158 return this; 159 } 160 161 @Override /* Microservice */ 162 public synchronized JettyMicroservice stop() throws Exception { 163 final Logger logger = getLogger(); 164 final MessageBundle mb2 = messages; 165 Thread t = new Thread("JettyMicroserviceStop") { 166 @Override /* Thread */ 167 public void run() { 168 try { 169 if (server == null || server.isStopping() || server.isStopped()) 170 return; 171 listener.onStopServer(JettyMicroservice.this); 172 out(mb2, "StoppingServer"); 173 server.stop(); 174 out(mb2, "ServerStopped"); 175 listener.onPostStopServer(JettyMicroservice.this); 176 } catch (Exception e) { 177 logger.log(Level.WARNING, e.getLocalizedMessage(), e); 178 } 179 } 180 }; 181 t.start(); 182 try { 183 t.join(); 184 } catch (InterruptedException e) { 185 e.printStackTrace(); 186 } 187 super.stop(); 188 189 return this; 190 } 191 192 193 //----------------------------------------------------------------------------------------------------------------- 194 // JettyMicroservice API methods. 195 //----------------------------------------------------------------------------------------------------------------- 196 197 /** 198 * Returns the port that this microservice started up on. 199 * <p> 200 * The value is determined by looking at the <code>Server/Connectors[ServerConnector]/port</code> value in the 201 * Jetty configuration. 202 * 203 * @return The port that this microservice started up on. 204 */ 205 public int getPort() { 206 for (Connector c : getServer().getConnectors()) 207 if (c instanceof ServerConnector) 208 return ((ServerConnector)c).getPort(); 209 throw new RuntimeException("Could not locate ServerConnector in Jetty server."); 210 } 211 212 /** 213 * Returns the context path that this microservice is using. 214 * <p> 215 * The value is determined by looking at the <code>Server/Handlers[ServletContextHandler]/contextPath</code> value 216 * in the Jetty configuration. 217 * 218 * @return The context path that this microservice is using. 219 */ 220 public String getContextPath() { 221 for (Handler h : getServer().getHandlers()) { 222 if (h instanceof HandlerCollection) 223 for (Handler h2 : ((HandlerCollection)h).getChildHandlers()) 224 if (h2 instanceof ServletContextHandler) 225 return ((ServletContextHandler)h2).getContextPath(); 226 if (h instanceof ServletContextHandler) 227 return ((ServletContextHandler)h).getContextPath(); 228 } 229 throw new RuntimeException("Could not locate ServletContextHandler in Jetty server."); 230 } 231 232 /** 233 * Returns whether this microservice is using <js>"http"</js> or <js>"https"</js>. 234 * <p> 235 * The value is determined by looking for the existence of an SSL Connection Factorie by looking for the 236 * <code>Server/Connectors[ServerConnector]/ConnectionFactories[SslConnectionFactory]</code> value in the Jetty 237 * configuration. 238 * 239 * @return Whether this microservice is using <js>"http"</js> or <js>"https"</js>. 240 */ 241 public String getProtocol() { 242 for (Connector c : getServer().getConnectors()) 243 if (c instanceof ServerConnector) 244 for (ConnectionFactory cf : ((ServerConnector)c).getConnectionFactories()) 245 if (cf instanceof SslConnectionFactory) 246 return "https"; 247 return "http"; 248 } 249 250 /** 251 * Returns the hostname of this microservice. 252 * <p> 253 * Simply uses <code>InetAddress.getLocalHost().getHostName()</code>. 254 * 255 * @return The hostname of this microservice. 256 */ 257 public String getHostName() { 258 String hostname = "localhost"; 259 try { 260 hostname = InetAddress.getLocalHost().getHostName(); 261 } catch (UnknownHostException e) {} 262 return hostname; 263 } 264 265 /** 266 * Returns the URI where this microservice is listening on. 267 * 268 * @return The URI where this microservice is listening on. 269 */ 270 public URI getURI() { 271 String cp = getContextPath(); 272 try { 273 return new URI(getProtocol(), null, getHostName(), getPort(), "/".equals(cp) ? null : cp, null, null); 274 } catch (URISyntaxException e) { 275 throw new RuntimeException(e); 276 } 277 } 278 279 /** 280 * Method used to create (but not start) an instance of a Jetty server. 281 * 282 * <p> 283 * Subclasses can override this method to customize the Jetty server before it is started. 284 * 285 * <p> 286 * The default implementation is configured by the following values in the config file 287 * if a jetty.xml is not specified via a <code>REST/jettyXml</code> setting: 288 * <p class='bcode w800'> 289 * <cc>#================================================================================ 290 * # Jetty settings 291 * #================================================================================</cc> 292 * <cs>[Jetty]</cs> 293 * 294 * <cc># Path of the jetty.xml file used to configure the Jetty server.</cc> 295 * <ck>config</ck> = jetty.xml 296 * 297 * <cc># Resolve Juneau variables in the jetty.xml file.</cc> 298 * <ck>resolveVars</ck> = true 299 * 300 * <cc># Port to use for the jetty server. 301 * # You can specify multiple ports. The first available will be used. '0' indicates to try a random port. 302 * # The resulting available port gets set as the system property "availablePort" which can be referenced in the 303 * # jetty.xml file as "$S{availablePort}" (assuming resolveVars is enabled).</cc> 304 * <ck>port</ck> = 10000,0,0,0 305 * </p> 306 * 307 * @return The newly-created server. 308 * @throws Exception 309 */ 310 public Server createServer() throws Exception { 311 listener.onCreateServer(this); 312 313 Config cf = getConfig(); 314 ObjectMap mf = getManifest(); 315 VarResolver vr = getVarResolver(); 316 317 int[] ports = ObjectUtils.firstNonNull(builder.ports, cf.getObjectWithDefault("Jetty/port", mf.getWithDefault("Jetty-Port", new int[]{8000}, int[].class), int[].class)); 318 int availablePort = findOpenPort(ports); 319 setProperty("availablePort", availablePort, false); 320 321 String jettyXml = builder.jettyXml; 322 String jettyConfig = cf.getString("Jetty/config", mf.getString("Jetty-Config", "jetty.xml")); 323 boolean resolveVars = ObjectUtils.firstNonNull(builder.jettyXmlResolveVars, cf.getBoolean("Jetty/resolveVars")); 324 325 if (jettyXml == null) 326 jettyXml = IOUtils.loadSystemResourceAsString("jetty.xml", ".", "files"); 327 if (jettyXml == null) 328 throw new FormattedRuntimeException("jetty.xml file ''{0}'' was not found on the file system or classpath.", jettyConfig); 329 330 if (resolveVars) 331 jettyXml = vr.resolve(jettyXml); 332 333 getLogger().info(jettyXml); 334 335 server = factory.create(jettyXml); 336 337 for (String s : cf.getStringArray("Jetty/servlets", new String[0])) { 338 Class<?> c = Class.forName(s); 339 if (ClassUtils.isParentClass(RestServlet.class, c)) { 340 RestServlet rs = (RestServlet)c.newInstance(); 341 addServlet(rs, rs.getPath()); 342 } else { 343 throw new FormattedRuntimeException("Invalid servlet specified in Jetty/servlets. Must be a subclass of RestServlet.", s); 344 } 345 } 346 347 for (Map.Entry<String,Object> e : cf.getObjectMap("Jetty/servletMap", ObjectMap.EMPTY_MAP).entrySet()) { 348 Class<?> c = Class.forName(e.getValue().toString()); 349 if (ClassUtils.isParentClass(Servlet.class, c)) { 350 Servlet rs = (Servlet)c.newInstance(); 351 addServlet(rs, e.getKey()); 352 } else { 353 throw new FormattedRuntimeException("Invalid servlet specified in Jetty/servletMap. Must be a subclass of Servlet.", e.getValue()); 354 } 355 } 356 357 for (Map.Entry<String,Object> e : cf.getObjectMap("Jetty/servletAttributes", ObjectMap.EMPTY_MAP).entrySet()) 358 addServletAttribute(e.getKey(), e.getValue()); 359 360 for (Map.Entry<String,Servlet> e : builder.servlets.entrySet()) 361 addServlet(e.getValue(), e.getKey()); 362 363 for (Map.Entry<String,Object> e : builder.servletAttributes.entrySet()) 364 addServletAttribute(e.getKey(), e.getValue()); 365 366 setProperty("juneau.serverPort", availablePort, false); 367 368 return server; 369 } 370 371 /** 372 * Calls {@link Server#destroy()} on the underlying Jetty server if it exists. 373 * 374 * @throws Exception 375 */ 376 public void destroyServer() throws Exception { 377 if (server != null) 378 server.destroy(); 379 server = null; 380 } 381 382 /** 383 * Adds an arbitrary servlet to this microservice. 384 * 385 * @param servlet The servlet instance. 386 * @param pathSpec The context path of the servlet. 387 * @return This object (for method chaining). 388 * @throws RuntimeException if {@link #createServer()} has not previously been called. 389 */ 390 public JettyMicroservice addServlet(Servlet servlet, String pathSpec) { 391 ServletHolder sh = new ServletHolder(servlet); 392 getServletContextHandler().addServlet(sh, pathSpec); 393 return this; 394 } 395 396 /** 397 * Finds and returns the servlet context handler define in the Jetty container. 398 * 399 * @return The servlet context handler. 400 * @throws RuntimeException if context handler is not defined. 401 */ 402 protected ServletContextHandler getServletContextHandler() { 403 for (Handler h : getServer().getHandlers()) { 404 ServletContextHandler sch = getServletContextHandler(h); 405 if (sch != null) 406 return sch; 407 } 408 throw new RuntimeException("Servlet context handler not found in jetty server."); 409 } 410 411 /** 412 * Adds a servlet attribute to the Jetty server. 413 * 414 * @param name The server attribute name. 415 * @param value The context path of the servlet. 416 * @return This object (for method chaining). 417 * @throws RuntimeException if {@link #createServer()} has not previously been called. 418 */ 419 public JettyMicroservice addServletAttribute(String name, Object value) { 420 getServer().setAttribute(name, value); 421 return this; 422 } 423 424 /** 425 * Returns the underlying Jetty server. 426 * 427 * @return The underlying Jetty server, or <jk>null</jk> if {@link #createServer()} has not yet been called. 428 */ 429 public Server getServer() { 430 if (server == null) 431 throw new RuntimeException("Server not found. createServer() must be called first."); 432 return server; 433 } 434 435 /** 436 * Method used to start the Jetty server created by {@link #createServer()}. 437 * 438 * <p> 439 * Subclasses can override this method to customize server startup. 440 * 441 * @return The port that this server started on. 442 * @throws Exception 443 */ 444 protected int startServer() throws Exception { 445 listener.onStartServer(this); 446 server.start(); 447 out(messages, "ServerStarted", getPort()); 448 listener.onPostStartServer(this); 449 return getPort(); 450 } 451 452 //----------------------------------------------------------------------------------------------------------------- 453 // Utility methods. 454 //----------------------------------------------------------------------------------------------------------------- 455 456 private static ServletContextHandler getServletContextHandler(Handler h) { 457 if (h instanceof ServletContextHandler) 458 return (ServletContextHandler)h; 459 if (h instanceof HandlerCollection) { 460 for (Handler h2 : ((HandlerCollection)h).getHandlers()) { 461 ServletContextHandler sch = getServletContextHandler(h2); 462 if (sch != null) 463 return sch; 464 } 465 } 466 return null; 467 } 468 469 private static int findOpenPort(int[] ports) { 470 for (int port : ports) { 471 // If port is 0, try a random port between ports[0] and 32767. 472 if (port == 0) 473 port = new Random().nextInt(32767 - ports[0] + 1) + ports[0]; 474 try (ServerSocket ss = new ServerSocket(port)) { 475 return port; 476 } catch (IOException e) {} 477 } 478 return 0; 479 } 480}