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}