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