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}