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.Locale;
024import java.util.Map;
025import java.util.Set;
026import java.util.concurrent.CountDownLatch;
027import java.util.concurrent.TimeUnit;
028import java.util.concurrent.atomic.AtomicBoolean;
029import java.util.concurrent.atomic.AtomicInteger;
030
031import org.apache.camel.CamelContext;
032import org.apache.camel.ProducerTemplate;
033import org.apache.camel.builder.RouteBuilder;
034import org.apache.camel.impl.DefaultModelJAXBContextFactory;
035import org.apache.camel.model.ModelCamelContext;
036import org.apache.camel.model.RouteDefinition;
037import org.apache.camel.spi.ModelJAXBContextFactory;
038import org.apache.camel.support.ServiceSupport;
039import org.apache.camel.util.ServiceHelper;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * Base class for main implementations to allow starting up a JVM with Camel embedded.
045 *
046 * @version 
047 */
048public abstract class MainSupport extends ServiceSupport {
049    protected static final Logger LOG = LoggerFactory.getLogger(MainSupport.class);
050    protected static final int UNINITIALIZED_EXIT_CODE = Integer.MIN_VALUE;
051    protected static final int DEFAULT_EXIT_CODE = 0;
052    protected final List<MainListener> listeners = new ArrayList<MainListener>();
053    protected final List<Option> options = new ArrayList<Option>();
054    protected final CountDownLatch latch = new CountDownLatch(1);
055    protected final AtomicBoolean completed = new AtomicBoolean(false);
056    protected final AtomicInteger exitCode = new AtomicInteger(UNINITIALIZED_EXIT_CODE);
057    protected long duration = -1;
058    protected TimeUnit timeUnit = TimeUnit.MILLISECONDS;
059    protected boolean trace;
060    protected List<RouteBuilder> routeBuilders = new ArrayList<RouteBuilder>();
061    protected String routeBuilderClasses;
062    protected final List<CamelContext> camelContexts = new ArrayList<CamelContext>();
063    protected ProducerTemplate camelTemplate;
064    protected boolean hangupInterceptorEnabled = true;
065    protected int durationHitExitCode = DEFAULT_EXIT_CODE;
066
067    /**
068     * A class for intercepting the hang up signal and do a graceful shutdown of the Camel.
069     */
070    private static final class HangupInterceptor extends Thread {
071        Logger log = LoggerFactory.getLogger(this.getClass());
072        final MainSupport mainInstance;
073
074        HangupInterceptor(MainSupport main) {
075            mainInstance = main;
076        }
077
078        @Override
079        public void run() {
080            log.info("Received hang up - stopping the main instance.");
081            try {
082                mainInstance.stop();
083            } catch (Exception ex) {
084                log.warn("Error during stopping the main instance.", ex);
085            }
086        }
087    }
088
089    protected MainSupport() {
090        addOption(new Option("h", "help", "Displays the help screen") {
091            protected void doProcess(String arg, LinkedList<String> remainingArgs) {
092                showOptions();
093                completed();
094            }
095        });
096        addOption(new ParameterOption("r", "routers",
097                 "Sets the router builder classes which will be loaded while starting the camel context",
098                 "routerBuilderClasses") {
099            @Override
100            protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) {
101                setRouteBuilderClasses(parameter);
102            }
103        });
104        addOption(new ParameterOption("d", "duration",
105                "Sets the time duration that the application will run for, by default in milliseconds. You can use '10s' for 10 seconds etc",
106                "duration") {
107            protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) {
108                String value = parameter.toUpperCase(Locale.ENGLISH);
109                if (value.endsWith("S")) {
110                    value = value.substring(0, value.length() - 1);
111                    setTimeUnit(TimeUnit.SECONDS);
112                }
113                setDuration(Integer.parseInt(value));
114            }
115        });
116        addOption(new Option("t", "trace", "Enables tracing") {
117            protected void doProcess(String arg, LinkedList<String> remainingArgs) {
118                enableTrace();
119            }
120        });
121        addOption(new ParameterOption("e", "exitcode",
122                "Sets the exit code if duration was hit",
123                "exitcode")  {
124            protected void doProcess(String arg, String parameter, LinkedList<String> remainingArgs) {
125                setDurationHitExitCode(Integer.parseInt(parameter));
126            }
127        });
128    }
129
130    /**
131     * Runs this process with the given arguments, and will wait until completed, or the JVM terminates.
132     */
133    public void run() throws Exception {
134        if (!completed.get()) {
135            internalBeforeStart();
136            // if we have an issue starting then propagate the exception to caller
137            beforeStart();
138            start();
139            try {
140                afterStart();
141                waitUntilCompleted();
142                internalBeforeStop();
143                beforeStop();
144                stop();
145                afterStop();
146            } catch (Exception e) {
147                // however while running then just log errors
148                LOG.error("Failed: " + e, e);
149            }
150        }
151    }
152
153    /**
154     * Disable the hangup support. No graceful stop by calling stop() on a
155     * Hangup signal.
156     */
157    public void disableHangupSupport() {
158        hangupInterceptorEnabled = false;
159    }
160
161    /**
162     * Hangup support is enabled by default.
163     *
164     * @deprecated is enabled by default now, so no longer need to call this method.
165     */
166    @Deprecated
167    public void enableHangupSupport() {
168        hangupInterceptorEnabled = true;
169    }
170
171    /**
172     * Adds a {@link org.apache.camel.main.MainListener} to receive callbacks when the main is started or stopping
173     *
174     * @param listener the listener
175     */
176    public void addMainListener(MainListener listener) {
177        listeners.add(listener);
178    }
179
180    /**
181     * Removes the {@link org.apache.camel.main.MainListener}
182     *
183     * @param listener the listener
184     */
185    public void removeMainListener(MainListener listener) {
186        listeners.remove(listener);
187    }
188
189    /**
190     * Callback to run custom logic before CamelContext is being started.
191     * <p/>
192     * It is recommended to use {@link org.apache.camel.main.MainListener} instead.
193     */
194    protected void beforeStart() throws Exception {
195        for (MainListener listener : listeners) {
196            listener.beforeStart(this);
197        }
198    }
199
200    /**
201     * Callback to run custom logic after CamelContext has been started.
202     * <p/>
203     * It is recommended to use {@link org.apache.camel.main.MainListener} instead.
204     */
205    protected void afterStart() throws Exception {
206        for (MainListener listener : listeners) {
207            listener.afterStart(this);
208        }
209    }
210
211    private void internalBeforeStart() {
212        if (hangupInterceptorEnabled) {
213            Runtime.getRuntime().addShutdownHook(new HangupInterceptor(this));
214        }
215    }
216
217    /**
218     * Callback to run custom logic before CamelContext is being stopped.
219     * <p/>
220     * It is recommended to use {@link org.apache.camel.main.MainListener} instead.
221     */
222    protected void beforeStop() throws Exception {
223        for (MainListener listener : listeners) {
224            listener.beforeStop(this);
225        }
226    }
227
228    /**
229     * Callback to run custom logic after CamelContext has been stopped.
230     * <p/>
231     * It is recommended to use {@link org.apache.camel.main.MainListener} instead.
232     */
233    protected void afterStop() throws Exception {
234        for (MainListener listener : listeners) {
235            listener.afterStop(this);
236        }
237    }
238
239    private void internalBeforeStop() {
240        try {
241            if (camelTemplate != null) {
242                ServiceHelper.stopService(camelTemplate);
243                camelTemplate = null;
244            }
245        } catch (Exception e) {
246            LOG.debug("Error stopping camelTemplate due " + e.getMessage() + ". This exception is ignored.", e);
247        }
248    }
249
250    /**
251     * Marks this process as being completed.
252     */
253    public void completed() {
254        completed.set(true);
255        exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, DEFAULT_EXIT_CODE);
256        latch.countDown();
257    }
258
259    /**
260     * Displays the command line options.
261     */
262    public void showOptions() {
263        showOptionsHeader();
264
265        for (Option option : options) {
266            System.out.println(option.getInformation());
267        }
268    }
269
270    /**
271     * Parses the command line arguments.
272     */
273    public void parseArguments(String[] arguments) {
274        LinkedList<String> args = new LinkedList<String>(Arrays.asList(arguments));
275
276        boolean valid = true;
277        while (!args.isEmpty()) {
278            String arg = args.removeFirst();
279
280            boolean handled = false;
281            for (Option option : options) {
282                if (option.processOption(arg, args)) {
283                    handled = true;
284                    break;
285                }
286            }
287            if (!handled) {
288                System.out.println("Unknown option: " + arg);
289                System.out.println();
290                valid = false;
291                break;
292            }
293        }
294        if (!valid) {
295            showOptions();
296            completed();
297        }
298    }
299
300    public void addOption(Option option) {
301        options.add(option);
302    }
303
304    public long getDuration() {
305        return duration;
306    }
307
308    /**
309     * Sets the duration to run the application for in milliseconds until it
310     * should be terminated. Defaults to -1. Any value <= 0 will run forever.
311     */
312    public void setDuration(long duration) {
313        this.duration = duration;
314    }
315
316    public TimeUnit getTimeUnit() {
317        return timeUnit;
318    }
319
320    /**
321     * Sets the time unit duration.
322     */
323    public void setTimeUnit(TimeUnit timeUnit) {
324        this.timeUnit = timeUnit;
325    }
326
327    /**
328     * Sets the exit code for the application if duration was hit
329     */
330    public void setDurationHitExitCode(int durationHitExitCode) {
331        this.durationHitExitCode = durationHitExitCode;
332    }
333
334    public int getDurationHitExitCode() {
335        return durationHitExitCode;
336    }
337
338    public int getExitCode() {
339        return exitCode.get();
340    }
341
342
343    public void setRouteBuilderClasses(String builders) {
344        this.routeBuilderClasses = builders;
345    }
346
347    public String getRouteBuilderClasses() {
348        return routeBuilderClasses;
349    }
350
351    public boolean isTrace() {
352        return trace;
353    }
354
355    public void enableTrace() {
356        this.trace = true;
357    }
358
359    protected void doStop() throws Exception {
360        // call completed to properly stop as we count down the waiting latch
361        completed();
362    }
363
364    protected void doStart() throws Exception {
365    }
366
367    protected void waitUntilCompleted() {
368        while (!completed.get()) {
369            try {
370                if (duration > 0) {
371                    TimeUnit unit = getTimeUnit();
372                    LOG.info("Waiting for: " + duration + " " + unit);
373                    latch.await(duration, unit);
374                    exitCode.compareAndSet(UNINITIALIZED_EXIT_CODE, durationHitExitCode);
375                    completed.set(true);
376                } else {
377                    latch.await();
378                }
379            } catch (InterruptedException e) {
380                Thread.currentThread().interrupt();
381            }
382        }
383    }
384
385    /**
386     * Parses the command line arguments then runs the program.
387     */
388    public void run(String[] args) throws Exception {
389        parseArguments(args);
390        run();
391    }
392
393    /**
394     * Displays the header message for the command line options.
395     */
396    public void showOptionsHeader() {
397        System.out.println("Apache Camel Runner takes the following options");
398        System.out.println();
399    }
400
401    public List<CamelContext> getCamelContexts() {
402        return camelContexts;
403    }
404
405    public List<RouteBuilder> getRouteBuilders() {
406        return routeBuilders;
407    }
408
409    public void setRouteBuilders(List<RouteBuilder> routeBuilders) {
410        this.routeBuilders = routeBuilders;
411    }
412
413    public List<RouteDefinition> getRouteDefinitions() {
414        List<RouteDefinition> answer = new ArrayList<RouteDefinition>();
415        for (CamelContext camelContext : camelContexts) {
416            answer.addAll(((ModelCamelContext)camelContext).getRouteDefinitions());
417        }
418        return answer;
419    }
420
421    public ProducerTemplate getCamelTemplate() throws Exception {
422        if (camelTemplate == null) {
423            camelTemplate = findOrCreateCamelTemplate();
424        }
425        return camelTemplate;
426    }
427
428    protected abstract ProducerTemplate findOrCreateCamelTemplate();
429
430    protected abstract Map<String, CamelContext> getCamelContextMap();
431
432    protected void postProcessContext() throws Exception {
433        Map<String, CamelContext> map = getCamelContextMap();
434        Set<Map.Entry<String, CamelContext>> entries = map.entrySet();
435        for (Map.Entry<String, CamelContext> entry : entries) {
436            CamelContext camelContext = entry.getValue();
437            camelContexts.add(camelContext);
438            postProcessCamelContext(camelContext);
439        }
440    }
441
442    public ModelJAXBContextFactory getModelJAXBContextFactory() {
443        return new DefaultModelJAXBContextFactory();
444    }
445
446    protected void loadRouteBuilders(CamelContext camelContext) throws Exception {
447        if (routeBuilderClasses != null) {
448            // get the list of route builder classes
449            String[] routeClasses = routeBuilderClasses.split(",");
450            for (String routeClass : routeClasses) {
451                Class<?> routeClazz = camelContext.getClassResolver().resolveClass(routeClass);
452                RouteBuilder builder = (RouteBuilder) routeClazz.newInstance();
453                getRouteBuilders().add(builder);
454            }
455        }
456    }
457
458    protected void postProcessCamelContext(CamelContext camelContext) throws Exception {
459        if (trace) {
460            camelContext.setTracing(true);
461        }
462        // try to load the route builders from the routeBuilderClasses
463        loadRouteBuilders(camelContext);
464        for (RouteBuilder routeBuilder : routeBuilders) {
465            camelContext.addRoutes(routeBuilder);
466        }
467        // register lifecycle so we are notified in Camel is stopped from JMX or somewhere else
468        camelContext.addLifecycleStrategy(new MainLifecycleStrategy(completed, latch));
469        // allow to do configuration before its started
470        for (MainListener listener : listeners) {
471            listener.configure(camelContext);
472        }
473    }
474
475    public void addRouteBuilder(RouteBuilder routeBuilder) {
476        getRouteBuilders().add(routeBuilder);
477    }
478
479    public abstract class Option {
480        private String abbreviation;
481        private String fullName;
482        private String description;
483
484        protected Option(String abbreviation, String fullName, String description) {
485            this.abbreviation = "-" + abbreviation;
486            this.fullName = "-" + fullName;
487            this.description = description;
488        }
489
490        public boolean processOption(String arg, LinkedList<String> remainingArgs) {
491            if (arg.equalsIgnoreCase(abbreviation) || fullName.startsWith(arg)) {
492                doProcess(arg, remainingArgs);
493                return true;
494            }
495            return false;
496        }
497
498        public String getAbbreviation() {
499            return abbreviation;
500        }
501
502        public String getDescription() {
503            return description;
504        }
505
506        public String getFullName() {
507            return fullName;
508        }
509
510        public String getInformation() {
511            return "  " + getAbbreviation() + " or " + getFullName() + " = " + getDescription();
512        }
513
514        protected abstract void doProcess(String arg, LinkedList<String> remainingArgs);
515    }
516
517    public abstract class ParameterOption extends Option {
518        private String parameterName;
519
520        protected ParameterOption(String abbreviation, String fullName, String description, String parameterName) {
521            super(abbreviation, fullName, description);
522            this.parameterName = parameterName;
523        }
524
525        protected void doProcess(String arg, LinkedList<String> remainingArgs) {
526            if (remainingArgs.isEmpty()) {
527                System.err.println("Expected fileName for ");
528                showOptions();
529                completed();
530            } else {
531                String parameter = remainingArgs.removeFirst();
532                doProcess(arg, parameter, remainingArgs);
533            }
534        }
535
536        public String getInformation() {
537            return "  " + getAbbreviation() + " or " + getFullName() + " <" + parameterName + "> = " + getDescription();
538        }
539
540        protected abstract void doProcess(String arg, String parameter, LinkedList<String> remainingArgs);
541    }
542}