001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.main; 018 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.Map; 024import java.util.Set; 025import java.util.concurrent.CountDownLatch; 026import java.util.concurrent.TimeUnit; 027import java.util.concurrent.atomic.AtomicBoolean; 028import java.util.concurrent.atomic.AtomicInteger; 029 030import org.apache.camel.CamelContext; 031import org.apache.camel.ProducerTemplate; 032import org.apache.camel.builder.RouteBuilder; 033import org.apache.camel.impl.DefaultModelJAXBContextFactory; 034import org.apache.camel.impl.FileWatcherReloadStrategy; 035import org.apache.camel.model.RouteDefinition; 036import org.apache.camel.spi.EventNotifier; 037import org.apache.camel.spi.ModelJAXBContextFactory; 038import org.apache.camel.spi.ReloadStrategy; 039import org.apache.camel.support.ServiceSupport; 040import org.apache.camel.util.ServiceHelper; 041import org.apache.camel.util.concurrent.ThreadHelper; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045/** 046 * Base class for main implementations to allow starting up a JVM with Camel embedded. 047 * 048 * @version 049 */ 050public abstract class MainSupport extends ServiceSupport { 051 protected static final Logger LOG = LoggerFactory.getLogger(MainSupport.class); 052 protected static final int UNINITIALIZED_EXIT_CODE = Integer.MIN_VALUE; 053 protected static final int DEFAULT_EXIT_CODE = 0; 054 protected final List<MainListener> listeners = new ArrayList<>(); 055 protected final List<Option> options = new ArrayList<>(); 056 protected final CountDownLatch latch = new CountDownLatch(1); 057 protected final AtomicBoolean completed = new AtomicBoolean(false); 058 protected final AtomicInteger exitCode = new AtomicInteger(UNINITIALIZED_EXIT_CODE); 059 protected long duration = -1; 060 protected long durationIdle = -1; 061 protected int durationMaxMessages; 062 protected TimeUnit timeUnit = TimeUnit.SECONDS; 063 protected boolean trace; 064 protected List<RouteBuilder> routeBuilders = new ArrayList<>(); 065 protected String routeBuilderClasses; 066 protected String fileWatchDirectory; 067 protected boolean fileWatchDirectoryRecursively; 068 protected final List<CamelContext> camelContexts = new ArrayList<>(); 069 protected ProducerTemplate camelTemplate; 070 protected boolean hangupInterceptorEnabled = true; 071 protected int durationHitExitCode = DEFAULT_EXIT_CODE; 072 protected ReloadStrategy reloadStrategy; 073 074 /** 075 * A class for intercepting the hang up signal and do a graceful shutdown of the Camel. 076 */ 077 private static final class HangupInterceptor extends Thread { 078 Logger log = LoggerFactory.getLogger(this.getClass()); 079 final MainSupport mainInstance; 080 081 HangupInterceptor(MainSupport main) { 082 mainInstance = main; 083 } 084 085 @Override 086 public void run() { 087 log.info("Received hang up - stopping the main instance."); 088 try { 089 mainInstance.stop(); 090 } catch (Exception ex) { 091 log.warn("Error during stopping the main instance.", ex); 092 } 093 } 094 } 095 096 protected MainSupport() { 097 addOption(new Option("h", "help", "Displays the help screen") { 098 protected void doProcess(String arg, LinkedList<String> remainingArgs) { 099 showOptions(); 100 completed(); 101 } 102 }); 103 addOption(new ParameterOption("r", "routers", 104 "Sets the router builder classes which will be loaded while starting the camel context", 105 "routerBuilderClasses") { 106 @Override 107 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 108 setRouteBuilderClasses(parameter); 109 } 110 }); 111 addOption(new ParameterOption("d", "duration", 112 "Sets the time duration (seconds) that the application will run for before terminating.", 113 "duration") { 114 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 115 // skip second marker to be backwards compatible 116 if (parameter.endsWith("s") || parameter.endsWith("S")) { 117 parameter = parameter.substring(0, parameter.length() - 1); 118 } 119 setDuration(Integer.parseInt(parameter)); 120 } 121 }); 122 addOption(new ParameterOption("dm", "durationMaxMessages", 123 "Sets the duration of maximum number of messages that the application will process before terminating.", 124 "durationMaxMessages") { 125 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 126 setDurationMaxMessages(Integer.parseInt(parameter)); 127 } 128 }); 129 addOption(new ParameterOption("di", "durationIdle", 130 "Sets the idle time duration (seconds) duration that the application can be idle before terminating.", 131 "durationIdle") { 132 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 133 // skip second marker to be backwards compatible 134 if (parameter.endsWith("s") || parameter.endsWith("S")) { 135 parameter = parameter.substring(0, parameter.length() - 1); 136 } 137 setDurationIdle(Integer.parseInt(parameter)); 138 } 139 }); 140 addOption(new Option("t", "trace", "Enables tracing") { 141 protected void doProcess(String arg, LinkedList<String> remainingArgs) { 142 enableTrace(); 143 } 144 }); 145 addOption(new ParameterOption("e", "exitcode", 146 "Sets the exit code if duration was hit", 147 "exitcode") { 148 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 149 setDurationHitExitCode(Integer.parseInt(parameter)); 150 } 151 }); 152 addOption(new ParameterOption("watch", "fileWatch", 153 "Sets a directory to watch for file changes to trigger reloading routes on-the-fly", 154 "fileWatch") { 155 @Override 156 protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { 157 setFileWatchDirectory(parameter); 158 } 159 }); 160 } 161 162 /** 163 * Runs this process with the given arguments, and will wait until completed, or the JVM terminates. 164 */ 165 public void run() throws Exception { 166 if (!completed.get()) { 167 internalBeforeStart(); 168 // if we have an issue starting then propagate the exception to caller 169 beforeStart(); 170 start(); 171 try { 172 afterStart(); 173 waitUntilCompleted(); 174 internalBeforeStop(); 175 beforeStop(); 176 stop(); 177 afterStop(); 178 } catch (Exception e) { 179 // however while running then just log errors 180 LOG.error("Failed: {}", e, e); 181 } 182 } 183 } 184 185 /** 186 * Disable the hangup support. No graceful stop by calling stop() on a 187 * Hangup signal. 188 */ 189 public void disableHangupSupport() { 190 hangupInterceptorEnabled = false; 191 } 192 193 /** 194 * Hangup support is enabled by default. 195 * 196 * @deprecated is enabled by default now, so no longer need to call this method. 197 */ 198 @Deprecated 199 public void enableHangupSupport() { 200 hangupInterceptorEnabled = true; 201 } 202 203 /** 204 * Adds a {@link org.apache.camel.main.MainListener} to receive callbacks when the main is started or stopping 205 * 206 * @param listener the listener 207 */ 208 public void addMainListener(MainListener listener) { 209 listeners.add(listener); 210 } 211 212 /** 213 * Removes the {@link org.apache.camel.main.MainListener} 214 * 215 * @param listener the listener 216 */ 217 public void removeMainListener(MainListener listener) { 218 listeners.remove(listener); 219 } 220 221 /** 222 * Callback to run custom logic before CamelContext is being started. 223 * <p/> 224 * It is recommended to use {@link org.apache.camel.main.MainListener} instead. 225 */ 226 protected void beforeStart() throws Exception { 227 for (MainListener listener : listeners) { 228 listener.beforeStart(this); 229 } 230 } 231 232 /** 233 * Callback to run custom logic after CamelContext has been started. 234 * <p/> 235 * It is recommended to use {@link org.apache.camel.main.MainListener} instead. 236 */ 237 protected void afterStart() throws Exception { 238 for (MainListener listener : listeners) { 239 listener.afterStart(this); 240 } 241 } 242 243 private void internalBeforeStart() { 244 if (hangupInterceptorEnabled) { 245 String threadName = ThreadHelper.resolveThreadName(null, "CamelHangupInterceptor"); 246 247 Thread task = new HangupInterceptor(this); 248 task.setName(threadName); 249 Runtime.getRuntime().addShutdownHook(task); 250 } 251 } 252 253 /** 254 * Callback to run custom logic before CamelContext is being stopped. 255 * <p/> 256 * It is recommended to use {@link org.apache.camel.main.MainListener} instead. 257 */ 258 protected void beforeStop() throws Exception { 259 for (MainListener listener : listeners) { 260 listener.beforeStop(this); 261 } 262 } 263 264 /** 265 * Callback to run custom logic after CamelContext has been stopped. 266 * <p/> 267 * It is recommended to use {@link org.apache.camel.main.MainListener} instead. 268 */ 269 protected void afterStop() throws Exception { 270 for (MainListener listener : listeners) { 271 listener.afterStop(this); 272 } 273 } 274 275 private void internalBeforeStop() { 276 try { 277 if (camelTemplate != null) { 278 ServiceHelper.stopService(camelTemplate); 279 camelTemplate = null; 280 } 281 } catch (Exception e) { 282 LOG.debug("Error stopping camelTemplate due " + e.getMessage() + ". This exception is ignored.", e); 283 } 284 } 285 286 /** 287 * Marks this process as being completed. 288 */ 289 public void completed() { 290 completed.set(true); 291 exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, DEFAULT_EXIT_CODE); 292 latch.countDown(); 293 } 294 295 /** 296 * Displays the command line options. 297 */ 298 public void showOptions() { 299 showOptionsHeader(); 300 301 for (Option option : options) { 302 System.out.println(option.getInformation()); 303 } 304 } 305 306 /** 307 * Parses the command line arguments. 308 */ 309 public void parseArguments(String[] arguments) { 310 LinkedList<String> args = new LinkedList<>(Arrays.asList(arguments)); 311 312 boolean valid = true; 313 while (!args.isEmpty()) { 314 String arg = args.removeFirst(); 315 316 boolean handled = false; 317 for (Option option : options) { 318 if (option.processOption(arg, args)) { 319 handled = true; 320 break; 321 } 322 } 323 if (!handled) { 324 System.out.println("Unknown option: " + arg); 325 System.out.println(); 326 valid = false; 327 break; 328 } 329 } 330 if (!valid) { 331 showOptions(); 332 completed(); 333 } 334 } 335 336 public void addOption(Option option) { 337 options.add(option); 338 } 339 340 public long getDuration() { 341 return duration; 342 } 343 344 /** 345 * Sets the duration (in seconds) to run the application until it 346 * should be terminated. Defaults to -1. Any value <= 0 will run forever. 347 */ 348 public void setDuration(long duration) { 349 this.duration = duration; 350 } 351 352 public long getDurationIdle() { 353 return durationIdle; 354 } 355 356 /** 357 * Sets the maximum idle duration (in seconds) when running the application, and 358 * if there has been no message processed after being idle for more than this duration 359 * then the application should be terminated. 360 * Defaults to -1. Any value <= 0 will run forever. 361 */ 362 public void setDurationIdle(long durationIdle) { 363 this.durationIdle = durationIdle; 364 } 365 366 public int getDurationMaxMessages() { 367 return durationMaxMessages; 368 } 369 370 /** 371 * Sets the duration to run the application to process at most max messages until it 372 * should be terminated. Defaults to -1. Any value <= 0 will run forever. 373 */ 374 public void setDurationMaxMessages(int durationMaxMessages) { 375 this.durationMaxMessages = durationMaxMessages; 376 } 377 378 public TimeUnit getTimeUnit() { 379 return timeUnit; 380 } 381 382 /** 383 * Sets the time unit duration (seconds by default). 384 */ 385 public void setTimeUnit(TimeUnit timeUnit) { 386 this.timeUnit = timeUnit; 387 } 388 389 /** 390 * Sets the exit code for the application if duration was hit 391 */ 392 public void setDurationHitExitCode(int durationHitExitCode) { 393 this.durationHitExitCode = durationHitExitCode; 394 } 395 396 public int getDurationHitExitCode() { 397 return durationHitExitCode; 398 } 399 400 public int getExitCode() { 401 return exitCode.get(); 402 } 403 404 public void setRouteBuilderClasses(String builders) { 405 this.routeBuilderClasses = builders; 406 } 407 408 public String getFileWatchDirectory() { 409 return fileWatchDirectory; 410 } 411 412 /** 413 * Sets the directory name to watch XML file changes to trigger live reload of Camel routes. 414 * <p/> 415 * Notice you cannot set this value and a custom {@link ReloadStrategy} as well. 416 */ 417 public void setFileWatchDirectory(String fileWatchDirectory) { 418 this.fileWatchDirectory = fileWatchDirectory; 419 } 420 421 public boolean isFileWatchDirectoryRecursively() { 422 return fileWatchDirectoryRecursively; 423 } 424 425 /** 426 * Sets the flag to watch directory of XML file changes recursively to trigger live reload of Camel routes. 427 * <p/> 428 * Notice you cannot set this value and a custom {@link ReloadStrategy} as well. 429 */ 430 public void setFileWatchDirectoryRecursively(boolean fileWatchDirectoryRecursively) { 431 this.fileWatchDirectoryRecursively = fileWatchDirectoryRecursively; 432 } 433 434 public String getRouteBuilderClasses() { 435 return routeBuilderClasses; 436 } 437 438 public ReloadStrategy getReloadStrategy() { 439 return reloadStrategy; 440 } 441 442 /** 443 * Sets a custom {@link ReloadStrategy} to be used. 444 * <p/> 445 * Notice you cannot set this value and the fileWatchDirectory as well. 446 */ 447 public void setReloadStrategy(ReloadStrategy reloadStrategy) { 448 this.reloadStrategy = reloadStrategy; 449 } 450 451 public boolean isTrace() { 452 return trace; 453 } 454 455 public void enableTrace() { 456 this.trace = true; 457 } 458 459 protected void doStop() throws Exception { 460 // call completed to properly stop as we count down the waiting latch 461 completed(); 462 } 463 464 protected void doStart() throws Exception { 465 } 466 467 protected void waitUntilCompleted() { 468 while (!completed.get()) { 469 try { 470 if (duration > 0) { 471 TimeUnit unit = getTimeUnit(); 472 LOG.info("Waiting for: {} {}", duration, unit); 473 latch.await(duration, unit); 474 exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); 475 completed.set(true); 476 } else if (durationIdle > 0) { 477 TimeUnit unit = getTimeUnit(); 478 LOG.info("Waiting to be idle for: {} {}", duration, unit); 479 exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); 480 latch.await(); 481 completed.set(true); 482 } else if (durationMaxMessages > 0) { 483 LOG.info("Waiting until: {} messages has been processed", durationMaxMessages); 484 exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); 485 latch.await(); 486 completed.set(true); 487 } else { 488 latch.await(); 489 } 490 } catch (InterruptedException e) { 491 Thread.currentThread().interrupt(); 492 } 493 } 494 } 495 496 /** 497 * Parses the command line arguments then runs the program. 498 */ 499 public void run(String[] args) throws Exception { 500 parseArguments(args); 501 run(); 502 LOG.info("MainSupport exiting code: {}", getExitCode()); 503 } 504 505 /** 506 * Displays the header message for the command line options. 507 */ 508 public void showOptionsHeader() { 509 System.out.println("Apache Camel Runner takes the following options"); 510 System.out.println(); 511 } 512 513 public List<CamelContext> getCamelContexts() { 514 return camelContexts; 515 } 516 517 public List<RouteBuilder> getRouteBuilders() { 518 return routeBuilders; 519 } 520 521 public void setRouteBuilders(List<RouteBuilder> routeBuilders) { 522 this.routeBuilders = routeBuilders; 523 } 524 525 public List<RouteDefinition> getRouteDefinitions() { 526 List<RouteDefinition> answer = new ArrayList<>(); 527 for (CamelContext camelContext : camelContexts) { 528 answer.addAll(camelContext.getRouteDefinitions()); 529 } 530 return answer; 531 } 532 533 public ProducerTemplate getCamelTemplate() throws Exception { 534 if (camelTemplate == null) { 535 camelTemplate = findOrCreateCamelTemplate(); 536 } 537 return camelTemplate; 538 } 539 540 protected abstract ProducerTemplate findOrCreateCamelTemplate(); 541 542 protected abstract Map<String, CamelContext> getCamelContextMap(); 543 544 protected void postProcessContext() throws Exception { 545 Map<String, CamelContext> map = getCamelContextMap(); 546 Set<Map.Entry<String, CamelContext>> entries = map.entrySet(); 547 for (Map.Entry<String, CamelContext> entry : entries) { 548 CamelContext camelContext = entry.getValue(); 549 camelContexts.add(camelContext); 550 postProcessCamelContext(camelContext); 551 } 552 } 553 554 public ModelJAXBContextFactory getModelJAXBContextFactory() { 555 return new DefaultModelJAXBContextFactory(); 556 } 557 558 protected void loadRouteBuilders(CamelContext camelContext) throws Exception { 559 if (routeBuilderClasses != null) { 560 // get the list of route builder classes 561 String[] routeClasses = routeBuilderClasses.split(","); 562 for (String routeClass : routeClasses) { 563 Class<?> routeClazz = camelContext.getClassResolver().resolveClass(routeClass); 564 RouteBuilder builder = (RouteBuilder) routeClazz.newInstance(); 565 getRouteBuilders().add(builder); 566 } 567 } 568 } 569 570 protected void postProcessCamelContext(CamelContext camelContext) throws Exception { 571 if (trace) { 572 camelContext.setTracing(true); 573 } 574 if (fileWatchDirectory != null) { 575 ReloadStrategy reload = new FileWatcherReloadStrategy(fileWatchDirectory, fileWatchDirectoryRecursively); 576 camelContext.setReloadStrategy(reload); 577 // ensure reload is added as service and started 578 camelContext.addService(reload); 579 // and ensure its register in JMX (which requires manually to be added because CamelContext is already started) 580 Object managedObject = camelContext.getManagementStrategy().getManagementObjectStrategy().getManagedObjectForService(camelContext, reload); 581 if (managedObject == null) { 582 // service should not be managed 583 return; 584 } 585 586 // skip already managed services, for example if a route has been restarted 587 if (camelContext.getManagementStrategy().isManaged(managedObject, null)) { 588 LOG.trace("The service is already managed: {}", reload); 589 return; 590 } 591 592 try { 593 camelContext.getManagementStrategy().manageObject(managedObject); 594 } catch (Exception e) { 595 LOG.warn("Could not register service: " + reload + " as Service MBean.", e); 596 } 597 } 598 599 if (durationMaxMessages > 0 || durationIdle > 0) { 600 // convert to seconds as that is what event notifier uses 601 long seconds = timeUnit.toSeconds(durationIdle); 602 // register lifecycle so we can trigger to shutdown the JVM when maximum number of messages has been processed 603 EventNotifier notifier = new MainDurationEventNotifier(camelContext, durationMaxMessages, seconds, completed, latch, true); 604 // register our event notifier 605 ServiceHelper.startService(notifier); 606 camelContext.getManagementStrategy().addEventNotifier(notifier); 607 } 608 609 // try to load the route builders from the routeBuilderClasses 610 loadRouteBuilders(camelContext); 611 for (RouteBuilder routeBuilder : routeBuilders) { 612 camelContext.addRoutes(routeBuilder); 613 } 614 // register lifecycle so we are notified in Camel is stopped from JMX or somewhere else 615 camelContext.addLifecycleStrategy(new MainLifecycleStrategy(completed, latch)); 616 // allow to do configuration before its started 617 for (MainListener listener : listeners) { 618 listener.configure(camelContext); 619 } 620 } 621 622 public void addRouteBuilder(RouteBuilder routeBuilder) { 623 getRouteBuilders().add(routeBuilder); 624 } 625 626 public abstract class Option { 627 private String abbreviation; 628 private String fullName; 629 private String description; 630 631 protected Option(String abbreviation, String fullName, String description) { 632 this.abbreviation = "-" + abbreviation; 633 this.fullName = "-" + fullName; 634 this.description = description; 635 } 636 637 public boolean processOption(String arg, LinkedList<String> remainingArgs) { 638 if (arg.equalsIgnoreCase(abbreviation) || fullName.startsWith(arg)) { 639 doProcess(arg, remainingArgs); 640 return true; 641 } 642 return false; 643 } 644 645 public String getAbbreviation() { 646 return abbreviation; 647 } 648 649 public String getDescription() { 650 return description; 651 } 652 653 public String getFullName() { 654 return fullName; 655 } 656 657 public String getInformation() { 658 return " " + getAbbreviation() + " or " + getFullName() + " = " + getDescription(); 659 } 660 661 protected abstract void doProcess(String arg, LinkedList<String> remainingArgs); 662 } 663 664 public abstract class ParameterOption extends Option { 665 private String parameterName; 666 667 protected ParameterOption(String abbreviation, String fullName, String description, String parameterName) { 668 super(abbreviation, fullName, description); 669 this.parameterName = parameterName; 670 } 671 672 protected void doProcess(String arg, LinkedList<String> remainingArgs) { 673 if (remainingArgs.isEmpty()) { 674 System.err.println("Expected fileName for "); 675 showOptions(); 676 completed(); 677 } else { 678 String parameter = remainingArgs.removeFirst(); 679 doProcess(arg, parameter, remainingArgs); 680 } 681 } 682 683 public String getInformation() { 684 return " " + getAbbreviation() + " or " + getFullName() + " <" + parameterName + "> = " + getDescription(); 685 } 686 687 protected abstract void doProcess(String arg, String parameter, LinkedList<String> remainingArgs); 688 } 689}