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