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