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