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