/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.main; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.camel.CamelContext; import org.apache.camel.ProducerTemplate; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.impl.DefaultModelJAXBContextFactory; import org.apache.camel.impl.FileWatcherReloadStrategy; import org.apache.camel.model.RouteDefinition; import org.apache.camel.spi.EventNotifier; import org.apache.camel.spi.ModelJAXBContextFactory; import org.apache.camel.spi.ReloadStrategy; import org.apache.camel.support.ServiceSupport; import org.apache.camel.util.ServiceHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base class for main implementations to allow starting up a JVM with Camel embedded. * * @version */ public abstract class MainSupport extends ServiceSupport { protected static final Logger LOG = LoggerFactory.getLogger(MainSupport.class); protected static final int UNINITIALIZED_EXIT_CODE = Integer.MIN_VALUE; protected static final int DEFAULT_EXIT_CODE = 0; protected final List<MainListener> listeners = new ArrayList<MainListener>(); protected final List<Option> options = new ArrayList<Option>(); protected final CountDownLatch latch = new CountDownLatch(1); protected final AtomicBoolean completed = new AtomicBoolean(false); protected final AtomicInteger exitCode = new AtomicInteger(UNINITIALIZED_EXIT_CODE); protected long duration = -1; protected long durationIdle = -1; protected int durationMaxMessages; protected TimeUnit timeUnit = TimeUnit.SECONDS; protected boolean trace; protected List<RouteBuilder> routeBuilders = new ArrayList<RouteBuilder>(); protected String routeBuilderClasses; protected String fileWatchDirectory; protected final List<CamelContext> camelContexts = new ArrayList<CamelContext>(); protected ProducerTemplate camelTemplate; protected boolean hangupInterceptorEnabled = true; protected int durationHitExitCode = DEFAULT_EXIT_CODE; protected ReloadStrategy reloadStrategy; /** * A class for intercepting the hang up signal and do a graceful shutdown of the Camel. */ private static final class HangupInterceptor extends Thread { Logger log = LoggerFactory.getLogger(this.getClass()); final MainSupport mainInstance; HangupInterceptor(MainSupport main) { mainInstance = main; } @Override public void run() { log.info("Received hang up - stopping the main instance."); try { mainInstance.stop(); } catch (Exception ex) { log.warn("Error during stopping the main instance.", ex); } } } protected MainSupport() { addOption(new Option("h", "help", "Displays the help screen") { protected void doProcess(String arg, LinkedList<String> remainingArgs) { showOptions(); completed(); } }); addOption(new ParameterOption("r", "routers", "Sets the router builder classes which will be loaded while starting the camel context", "routerBuilderClasses") { @Override protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { setRouteBuilderClasses(parameter); } }); addOption(new ParameterOption("d", "duration", "Sets the time duration (seconds) that the application will run for before terminating.", "duration") { protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { // skip second marker to be backwards compatible if (parameter.endsWith("s") || parameter.endsWith("S")) { parameter = parameter.substring(0, parameter.length() - 1); } setDuration(Integer.parseInt(parameter)); } }); addOption(new ParameterOption("dm", "durationMaxMessages", "Sets the duration of maximum number of messages that the application will process before terminating.", "durationMaxMessages") { protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { setDurationMaxMessages(Integer.parseInt(parameter)); } }); addOption(new ParameterOption("di", "durationIdle", "Sets the idle time duration (seconds) duration that the application can be idle before terminating.", "durationIdle") { protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { // skip second marker to be backwards compatible if (parameter.endsWith("s") || parameter.endsWith("S")) { parameter = parameter.substring(0, parameter.length() - 1); } setDurationIdle(Integer.parseInt(parameter)); } }); addOption(new Option("t", "trace", "Enables tracing") { protected void doProcess(String arg, LinkedList<String> remainingArgs) { enableTrace(); } }); addOption(new ParameterOption("e", "exitcode", "Sets the exit code if duration was hit", "exitcode") { protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { setDurationHitExitCode(Integer.parseInt(parameter)); } }); addOption(new ParameterOption("watch", "fileWatch", "Sets a directory to watch for file changes to trigger reloading routes on-the-fly", "fileWatch") { @Override protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) { setFileWatchDirectory(parameter); } }); } /** * Runs this process with the given arguments, and will wait until completed, or the JVM terminates. */ public void run() throws Exception { if (!completed.get()) { internalBeforeStart(); // if we have an issue starting then propagate the exception to caller beforeStart(); start(); try { afterStart(); waitUntilCompleted(); internalBeforeStop(); beforeStop(); stop(); afterStop(); } catch (Exception e) { // however while running then just log errors LOG.error("Failed: " + e, e); } } } /** * Disable the hangup support. No graceful stop by calling stop() on a * Hangup signal. */ public void disableHangupSupport() { hangupInterceptorEnabled = false; } /** * Hangup support is enabled by default. * * @deprecated is enabled by default now, so no longer need to call this method. */ @Deprecated public void enableHangupSupport() { hangupInterceptorEnabled = true; } /** * Adds a {@link org.apache.camel.main.MainListener} to receive callbacks when the main is started or stopping * * @param listener the listener */ public void addMainListener(MainListener listener) { listeners.add(listener); } /** * Removes the {@link org.apache.camel.main.MainListener} * * @param listener the listener */ public void removeMainListener(MainListener listener) { listeners.remove(listener); } /** * Callback to run custom logic before CamelContext is being started. * <p/> * It is recommended to use {@link org.apache.camel.main.MainListener} instead. */ protected void beforeStart() throws Exception { for (MainListener listener : listeners) { listener.beforeStart(this); } } /** * Callback to run custom logic after CamelContext has been started. * <p/> * It is recommended to use {@link org.apache.camel.main.MainListener} instead. */ protected void afterStart() throws Exception { for (MainListener listener : listeners) { listener.afterStart(this); } } private void internalBeforeStart() { if (hangupInterceptorEnabled) { Runtime.getRuntime().addShutdownHook(new HangupInterceptor(this)); } } /** * Callback to run custom logic before CamelContext is being stopped. * <p/> * It is recommended to use {@link org.apache.camel.main.MainListener} instead. */ protected void beforeStop() throws Exception { for (MainListener listener : listeners) { listener.beforeStop(this); } } /** * Callback to run custom logic after CamelContext has been stopped. * <p/> * It is recommended to use {@link org.apache.camel.main.MainListener} instead. */ protected void afterStop() throws Exception { for (MainListener listener : listeners) { listener.afterStop(this); } } private void internalBeforeStop() { try { if (camelTemplate != null) { ServiceHelper.stopService(camelTemplate); camelTemplate = null; } } catch (Exception e) { LOG.debug("Error stopping camelTemplate due " + e.getMessage() + ". This exception is ignored.", e); } } /** * Marks this process as being completed. */ public void completed() { completed.set(true); exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, DEFAULT_EXIT_CODE); latch.countDown(); } /** * Displays the command line options. */ public void showOptions() { showOptionsHeader(); for (Option option : options) { System.out.println(option.getInformation()); } } /** * Parses the command line arguments. */ public void parseArguments(String[] arguments) { LinkedList<String> args = new LinkedList<String>(Arrays.asList(arguments)); boolean valid = true; while (!args.isEmpty()) { String arg = args.removeFirst(); boolean handled = false; for (Option option : options) { if (option.processOption(arg, args)) { handled = true; break; } } if (!handled) { System.out.println("Unknown option: " + arg); System.out.println(); valid = false; break; } } if (!valid) { showOptions(); completed(); } } public void addOption(Option option) { options.add(option); } public long getDuration() { return duration; } /** * Sets the duration (in seconds) to run the application until it * should be terminated. Defaults to -1. Any value <= 0 will run forever. */ public void setDuration(long duration) { this.duration = duration; } public long getDurationIdle() { return durationIdle; } /** * Sets the maximum idle duration (in seconds) when running the application, and * if there has been no message processed after being idle for more than this duration * then the application should be terminated. * Defaults to -1. Any value <= 0 will run forever. */ public void setDurationIdle(long durationIdle) { this.durationIdle = durationIdle; } public int getDurationMaxMessages() { return durationMaxMessages; } /** * Sets the duration to run the application to process at most max messages until it * should be terminated. Defaults to -1. Any value <= 0 will run forever. */ public void setDurationMaxMessages(int durationMaxMessages) { this.durationMaxMessages = durationMaxMessages; } public TimeUnit getTimeUnit() { return timeUnit; } /** * Sets the time unit duration (seconds by default). */ public void setTimeUnit(TimeUnit timeUnit) { this.timeUnit = timeUnit; } /** * Sets the exit code for the application if duration was hit */ public void setDurationHitExitCode(int durationHitExitCode) { this.durationHitExitCode = durationHitExitCode; } public int getDurationHitExitCode() { return durationHitExitCode; } public int getExitCode() { return exitCode.get(); } public void setRouteBuilderClasses(String builders) { this.routeBuilderClasses = builders; } public String getFileWatchDirectory() { return fileWatchDirectory; } /** * Sets the directory name to watch XML file changes to trigger live reload of Camel routes. * <p/> * Notice you cannot set this value and a custom {@link ReloadStrategy} as well. */ public void setFileWatchDirectory(String fileWatchDirectory) { this.fileWatchDirectory = fileWatchDirectory; } public String getRouteBuilderClasses() { return routeBuilderClasses; } public ReloadStrategy getReloadStrategy() { return reloadStrategy; } /** * Sets a custom {@link ReloadStrategy} to be used. * <p/> * Notice you cannot set this value and the fileWatchDirectory as well. */ public void setReloadStrategy(ReloadStrategy reloadStrategy) { this.reloadStrategy = reloadStrategy; } public boolean isTrace() { return trace; } public void enableTrace() { this.trace = true; } protected void doStop() throws Exception { // call completed to properly stop as we count down the waiting latch completed(); } protected void doStart() throws Exception { } protected void waitUntilCompleted() { while (!completed.get()) { try { if (duration > 0) { TimeUnit unit = getTimeUnit(); LOG.info("Waiting for: " + duration + " " + unit); latch.await(duration, unit); exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); completed.set(true); } else if (durationIdle > 0) { TimeUnit unit = getTimeUnit(); LOG.info("Waiting to be idle for: " + duration + " " + unit); exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); latch.await(); completed.set(true); } else if (durationMaxMessages > 0) { LOG.info("Waiting until: " + durationMaxMessages + " messages has been processed"); exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode); latch.await(); completed.set(true); } else { latch.await(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } /** * Parses the command line arguments then runs the program. */ public void run(String[] args) throws Exception { parseArguments(args); run(); LOG.info("MainSupport exiting code: {}", getExitCode()); } /** * Displays the header message for the command line options. */ public void showOptionsHeader() { System.out.println("Apache Camel Runner takes the following options"); System.out.println(); } public List<CamelContext> getCamelContexts() { return camelContexts; } public List<RouteBuilder> getRouteBuilders() { return routeBuilders; } public void setRouteBuilders(List<RouteBuilder> routeBuilders) { this.routeBuilders = routeBuilders; } public List<RouteDefinition> getRouteDefinitions() { List<RouteDefinition> answer = new ArrayList<RouteDefinition>(); for (CamelContext camelContext : camelContexts) { answer.addAll(camelContext.getRouteDefinitions()); } return answer; } public ProducerTemplate getCamelTemplate() throws Exception { if (camelTemplate == null) { camelTemplate = findOrCreateCamelTemplate(); } return camelTemplate; } protected abstract ProducerTemplate findOrCreateCamelTemplate(); protected abstract Map<String, CamelContext> getCamelContextMap(); protected void postProcessContext() throws Exception { Map<String, CamelContext> map = getCamelContextMap(); Set<Map.Entry<String, CamelContext>> entries = map.entrySet(); for (Map.Entry<String, CamelContext> entry : entries) { CamelContext camelContext = entry.getValue(); camelContexts.add(camelContext); postProcessCamelContext(camelContext); } } public ModelJAXBContextFactory getModelJAXBContextFactory() { return new DefaultModelJAXBContextFactory(); } protected void loadRouteBuilders(CamelContext camelContext) throws Exception { if (routeBuilderClasses != null) { // get the list of route builder classes String[] routeClasses = routeBuilderClasses.split(","); for (String routeClass : routeClasses) { Class<?> routeClazz = camelContext.getClassResolver().resolveClass(routeClass); RouteBuilder builder = (RouteBuilder) routeClazz.newInstance(); getRouteBuilders().add(builder); } } } protected void postProcessCamelContext(CamelContext camelContext) throws Exception { if (trace) { camelContext.setTracing(true); } if (fileWatchDirectory != null) { ReloadStrategy reload = new FileWatcherReloadStrategy(fileWatchDirectory); camelContext.setReloadStrategy(reload); // ensure reload is added as service and started camelContext.addService(reload); // and ensure its register in JMX (which requires manually to be added because CamelContext is already started) Object managedObject = camelContext.getManagementStrategy().getManagementObjectStrategy().getManagedObjectForService(camelContext, reload); if (managedObject == null) { // service should not be managed return; } // skip already managed services, for example if a route has been restarted if (camelContext.getManagementStrategy().isManaged(managedObject, null)) { LOG.trace("The service is already managed: {}", reload); return; } try { camelContext.getManagementStrategy().manageObject(managedObject); } catch (Exception e) { LOG.warn("Could not register service: " + reload + " as Service MBean.", e); } } if (durationMaxMessages > 0 || durationIdle > 0) { // convert to seconds as that is what event notifier uses long seconds = timeUnit.toSeconds(durationIdle); // register lifecycle so we can trigger to shutdown the JVM when maximum number of messages has been processed EventNotifier notifier = new MainDurationEventNotifier(camelContext, durationMaxMessages, seconds, completed, latch, true); // register our event notifier ServiceHelper.startService(notifier); camelContext.getManagementStrategy().addEventNotifier(notifier); } // try to load the route builders from the routeBuilderClasses loadRouteBuilders(camelContext); for (RouteBuilder routeBuilder : routeBuilders) { camelContext.addRoutes(routeBuilder); } // register lifecycle so we are notified in Camel is stopped from JMX or somewhere else camelContext.addLifecycleStrategy(new MainLifecycleStrategy(completed, latch)); // allow to do configuration before its started for (MainListener listener : listeners) { listener.configure(camelContext); } } public void addRouteBuilder(RouteBuilder routeBuilder) { getRouteBuilders().add(routeBuilder); } public abstract class Option { private String abbreviation; private String fullName; private String description; protected Option(String abbreviation, String fullName, String description) { this.abbreviation = "-" + abbreviation; this.fullName = "-" + fullName; this.description = description; } public boolean processOption(String arg, LinkedList<String> remainingArgs) { if (arg.equalsIgnoreCase(abbreviation) || fullName.startsWith(arg)) { doProcess(arg, remainingArgs); return true; } return false; } public String getAbbreviation() { return abbreviation; } public String getDescription() { return description; } public String getFullName() { return fullName; } public String getInformation() { return " " + getAbbreviation() + " or " + getFullName() + " = " + getDescription(); } protected abstract void doProcess(String arg, LinkedList<String> remainingArgs); } public abstract class ParameterOption extends Option { private String parameterName; protected ParameterOption(String abbreviation, String fullName, String description, String parameterName) { super(abbreviation, fullName, description); this.parameterName = parameterName; } protected void doProcess(String arg, LinkedList<String> remainingArgs) { if (remainingArgs.isEmpty()) { System.err.println("Expected fileName for "); showOptions(); completed(); } else { String parameter = remainingArgs.removeFirst(); doProcess(arg, parameter, remainingArgs); } } public String getInformation() { return " " + getAbbreviation() + " or " + getFullName() + " <" + parameterName + "> = " + getDescription(); } protected abstract void doProcess(String arg, String parameter, LinkedList<String> remainingArgs); } }