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