/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.camel.spring.boot; import java.io.FileNotFoundException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.CamelContext; import org.apache.camel.RoutesBuilder; import org.apache.camel.main.MainDurationEventNotifier; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; import org.apache.camel.model.rest.RestDefinition; import org.apache.camel.model.rest.RestsDefinition; import org.apache.camel.spi.EventNotifier; import org.apache.camel.util.ServiceHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.core.io.Resource; /** * Collects routes and rests from the various sources (like Spring application context beans registry or opinionated * classpath locations) and injects these into the Camel context. */ public class RoutesCollector implements ApplicationListener<ContextRefreshedEvent> { // Static collaborators private static final Logger LOG = LoggerFactory.getLogger(RoutesCollector.class); // Collaborators private final ApplicationContext applicationContext; private final List<CamelContextConfiguration> camelContextConfigurations; private final CamelConfigurationProperties configurationProperties; // Constructors public RoutesCollector(ApplicationContext applicationContext, List<CamelContextConfiguration> camelContextConfigurations, CamelConfigurationProperties configurationProperties) { this.applicationContext = applicationContext; this.camelContextConfigurations = new ArrayList<CamelContextConfiguration>(camelContextConfigurations); this.configurationProperties = configurationProperties; } // Overridden @Override public void onApplicationEvent(ContextRefreshedEvent event) { ApplicationContext applicationContext = event.getApplicationContext(); // only listen to context refresh of "my" applicationContext if (this.applicationContext.equals(applicationContext)) { CamelContext camelContext = event.getApplicationContext().getBean(CamelContext.class); // only add and start Camel if its stopped (initial state) if (camelContext.getStatus().isStopped()) { LOG.debug("Post-processing CamelContext bean: {}", camelContext.getName()); for (RoutesBuilder routesBuilder : applicationContext.getBeansOfType(RoutesBuilder.class, configurationProperties.isIncludeNonSingletons(), true).values()) { // filter out abstract classes boolean abs = Modifier.isAbstract(routesBuilder.getClass().getModifiers()); if (!abs) { try { LOG.debug("Injecting following route into the CamelContext: {}", routesBuilder); camelContext.addRoutes(routesBuilder); } catch (Exception e) { throw new CamelSpringBootInitializationException(e); } } } try { boolean scan = !configurationProperties.getXmlRoutes().equals("false"); if (scan) { loadXmlRoutes(applicationContext, camelContext, configurationProperties.getXmlRoutes()); } boolean scanRests = !configurationProperties.getXmlRests().equals("false"); if (scanRests) { loadXmlRests(applicationContext, camelContext, configurationProperties.getXmlRests()); } for (CamelContextConfiguration camelContextConfiguration : camelContextConfigurations) { LOG.debug("CamelContextConfiguration found. Invoking beforeApplicationStart: {}", camelContextConfiguration); camelContextConfiguration.beforeApplicationStart(camelContext); } if (configurationProperties.isMainRunController()) { CamelMainRunController controller = new CamelMainRunController(applicationContext, camelContext); if (configurationProperties.getDurationMaxMessages() > 0 || configurationProperties.getDurationMaxIdleSeconds() > 0) { if (configurationProperties.getDurationMaxMessages() > 0) { LOG.info("CamelSpringBoot will terminate after processing {} messages", configurationProperties.getDurationMaxMessages()); } if (configurationProperties.getDurationMaxIdleSeconds() > 0) { LOG.info("CamelSpringBoot will terminate after being idle for more {} seconds", configurationProperties.getDurationMaxIdleSeconds()); } // register lifecycle so we can trigger to shutdown the JVM when maximum number of messages has been processed EventNotifier notifier = new MainDurationEventNotifier(camelContext, configurationProperties.getDurationMaxMessages(), configurationProperties.getDurationMaxIdleSeconds(), controller.getCompleted(), controller.getLatch(), true); // register our event notifier ServiceHelper.startService(notifier); camelContext.getManagementStrategy().addEventNotifier(notifier); } if (configurationProperties.getDurationMaxSeconds() > 0) { LOG.info("CamelSpringBoot will terminate after {} seconds", configurationProperties.getDurationMaxSeconds()); terminateMainControllerAfter(camelContext, configurationProperties.getDurationMaxSeconds(), controller.getCompleted(), controller.getLatch()); } // controller will start Camel LOG.info("Starting CamelMainRunController to ensure the main thread keeps running"); controller.start(); } else { if (applicationContext instanceof ConfigurableApplicationContext) { ConfigurableApplicationContext cac = (ConfigurableApplicationContext) applicationContext; if (configurationProperties.getDurationMaxSeconds() > 0) { LOG.info("CamelSpringBoot will terminate after {} seconds", configurationProperties.getDurationMaxSeconds()); terminateApplicationContext(cac, camelContext, configurationProperties.getDurationMaxSeconds()); } if (configurationProperties.getDurationMaxMessages() > 0 || configurationProperties.getDurationMaxIdleSeconds() > 0) { if (configurationProperties.getDurationMaxMessages() > 0) { LOG.info("CamelSpringBoot will terminate after processing {} messages", configurationProperties.getDurationMaxMessages()); } if (configurationProperties.getDurationMaxIdleSeconds() > 0) { LOG.info("CamelSpringBoot will terminate after being idle for more {} seconds", configurationProperties.getDurationMaxIdleSeconds()); } // needed by MainDurationEventNotifier to signal when we have processed the max messages final AtomicBoolean completed = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); // register lifecycle so we can trigger to shutdown the JVM when maximum number of messages has been processed EventNotifier notifier = new MainDurationEventNotifier(camelContext, configurationProperties.getDurationMaxMessages(), configurationProperties.getDurationMaxIdleSeconds(), completed, latch, false); // register our event notifier ServiceHelper.startService(notifier); camelContext.getManagementStrategy().addEventNotifier(notifier); terminateApplicationContext(cac, camelContext, latch); } } // start camel manually maybeStart(camelContext); } for (CamelContextConfiguration camelContextConfiguration : camelContextConfigurations) { LOG.debug("CamelContextConfiguration found. Invoking afterApplicationStart: {}", camelContextConfiguration); camelContextConfiguration.afterApplicationStart(camelContext); } } catch (Exception e) { throw new CamelSpringBootInitializationException(e); } } else { LOG.debug("Camel already started, not adding routes."); } } else { LOG.debug("Ignore ContextRefreshedEvent: {}", event); } } private void maybeStart(CamelContext camelContext) throws Exception { // for example from unit testing we want to start Camel later and not when Spring framework // publish a ContextRefreshedEvent boolean skip = "true".equalsIgnoreCase(System.getProperty("skipStartingCamelContext")); if (skip) { LOG.info("Skipping starting CamelContext as system property skipStartingCamelContext is set to be true."); } else { camelContext.start(); } } // Helpers private void loadXmlRoutes(ApplicationContext applicationContext, CamelContext camelContext, String directory) throws Exception { LOG.info("Loading additional Camel XML routes from: {}", directory); try { Resource[] xmlRoutes = applicationContext.getResources(directory); for (Resource xmlRoute : xmlRoutes) { LOG.debug("Found XML route: {}", xmlRoute); RoutesDefinition xmlDefinition = camelContext.loadRoutesDefinition(xmlRoute.getInputStream()); camelContext.addRouteDefinitions(xmlDefinition.getRoutes()); } } catch (FileNotFoundException e) { LOG.debug("No XML routes found in {}. Skipping XML routes detection.", directory); } } private void loadXmlRests(ApplicationContext applicationContext, CamelContext camelContext, String directory) { LOG.info("Loading additional Camel XML rests from: {}", directory); try { final Resource[] xmlRests = applicationContext.getResources(directory); for (final Resource xmlRest : xmlRests) { final RestsDefinition xmlDefinitions = camelContext.loadRestsDefinition(xmlRest.getInputStream()); camelContext.addRestDefinitions(xmlDefinitions.getRests()); for (final RestDefinition xmlDefinition : xmlDefinitions.getRests()) { final List<RouteDefinition> routeDefinitions = xmlDefinition.asRouteDefinition(camelContext); camelContext.addRouteDefinitions(routeDefinitions); } } } catch (FileNotFoundException e) { LOG.debug("No XML rests found in {}. Skipping XML rests detection.", directory); } catch (Exception e) { throw new RuntimeException(e); } } private void terminateMainControllerAfter(final CamelContext camelContext, int seconds, final AtomicBoolean completed, final CountDownLatch latch) { ScheduledExecutorService executorService = camelContext.getExecutorServiceManager().newSingleThreadScheduledExecutor(this, "CamelSpringBootTerminateTask"); Runnable task = () -> { LOG.info("CamelSpringBoot triggering shutdown of the JVM."); try { camelContext.stop(); } catch (Throwable e) { LOG.warn("Error during stopping CamelContext", e); } finally { completed.set(true); latch.countDown(); } }; executorService.schedule(task, seconds, TimeUnit.SECONDS); } private void terminateApplicationContext(final ConfigurableApplicationContext applicationContext, final CamelContext camelContext, int seconds) { ScheduledExecutorService executorService = camelContext.getExecutorServiceManager().newSingleThreadScheduledExecutor(this, "CamelSpringBootTerminateTask"); Runnable task = () -> { LOG.info("CamelSpringBoot triggering shutdown of the JVM."); // we need to run a daemon thread to stop ourselves so this thread pool can be stopped nice also new Thread(applicationContext::close).start(); }; executorService.schedule(task, seconds, TimeUnit.SECONDS); } private void terminateApplicationContext(final ConfigurableApplicationContext applicationContext, final CamelContext camelContext, final CountDownLatch latch) { ExecutorService executorService = camelContext.getExecutorServiceManager().newSingleThreadExecutor(this, "CamelSpringBootTerminateTask"); Runnable task = () -> { try { latch.await(); LOG.info("CamelSpringBoot triggering shutdown of the JVM."); // we need to run a daemon thread to stop ourselves so this thread pool can be stopped nice also new Thread(applicationContext::close).start(); } catch (Throwable e) { // ignore } }; executorService.submit(task); } }