/** * 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.support; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.apache.camel.CamelContext; import org.apache.camel.api.management.ManagedAttribute; import org.apache.camel.api.management.ManagedOperation; import org.apache.camel.model.ModelHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; import org.apache.camel.spi.ReloadStrategy; import org.apache.camel.util.CollectionStringBuffer; import org.apache.camel.util.LRUCache; import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.StringHelper; import org.apache.camel.util.XmlLineNumberParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Base class for implementing custom {@link ReloadStrategy} SPI plugins. */ public abstract class ReloadStrategySupport extends ServiceSupport implements ReloadStrategy { protected final Logger log = LoggerFactory.getLogger(getClass()); private CamelContext camelContext; // store state private final Map<String, ResourceState> cache = new LRUCache<String, ResourceState>(100); private int succeeded; private int failed; @Override public CamelContext getCamelContext() { return camelContext; } @Override public void setCamelContext(CamelContext camelContext) { this.camelContext = camelContext; } @Override public void onReloadXml(CamelContext camelContext, String name, InputStream resource) { log.debug("Reloading routes from XML resource: {}", name); Document dom; String xml; try { xml = camelContext.getTypeConverter().mandatoryConvertTo(String.class, resource); // the JAXB model expects the spring namespace (even for blueprint) dom = XmlLineNumberParser.parseXml(new ByteArrayInputStream(xml.getBytes()), null, "camelContext,routeContext,routes", "http://camel.apache.org/schema/spring"); } catch (Exception e) { failed++; log.warn("Cannot load the resource " + name + " as XML"); return; } ResourceState state = cache.get(name); if (state == null) { state = new ResourceState(name, dom, xml); cache.put(name, state); } String oldXml = state.getXml(); List<Integer> changed = StringHelper.changedLines(oldXml, xml); // find all <route> which are the routes NodeList list = dom.getElementsByTagName("route"); // collect which routes are updated/skipped List<RouteDefinition> routes = new ArrayList<>(); if (list != null && list.getLength() > 0) { for (int i = 0; i < list.getLength(); i++) { Node node = list.item(i); // what line number are this within String lineNumber = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER); String lineNumberEnd = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER_END); if (lineNumber != null && lineNumberEnd != null && !changed.isEmpty()) { int start = Integer.valueOf(lineNumber); int end = Integer.valueOf(lineNumberEnd); boolean within = withinChanged(start, end, changed); if (within) { log.debug("Updating route in lines: {}-{}", start, end); } else { log.debug("No changes to route in lines: {}-{}", start, end); continue; } } try { // load from XML -> JAXB model and store as routes to be updated RoutesDefinition loaded = ModelHelper.loadRoutesDefinition(camelContext, node); if (!loaded.getRoutes().isEmpty()) { routes.addAll(loaded.getRoutes()); } } catch (Exception e) { failed++; throw ObjectHelper.wrapRuntimeCamelException(e); } } } if (!routes.isEmpty()) { try { boolean unassignedRouteIds = false; CollectionStringBuffer csb = new CollectionStringBuffer(","); // collect route ids and force assign ids if not in use for (RouteDefinition route : routes) { unassignedRouteIds |= !route.hasCustomIdAssigned(); String id = route.idOrCreate(camelContext.getNodeIdFactory()); csb.append(id); } log.debug("Reloading routes: [{}] from XML resource: {}", csb, name); if (unassignedRouteIds) { log.warn("Routes with no id's detected. Its recommended to assign id's to your routes so Camel can reload the routes correctly."); } // update the routes (add will remove and shutdown first) camelContext.addRouteDefinitions(routes); log.info("Reloaded routes: [{}] from XML resource: {}", csb, name); } catch (Exception e) { failed++; throw ObjectHelper.wrapRuntimeCamelException(e); } } // update cache state = new ResourceState(name, dom, xml); cache.put(name, state); succeeded++; } private boolean withinChanged(int start, int end, List<Integer> changed) { for (int change : changed) { log.trace("Changed line: {} within {}-{}", change, start, end); if (change >= start && change <= end) { return true; } } return false; } @ManagedAttribute(description = "Number of reloads succeeded") public int getReloadCounter() { return succeeded; } @ManagedAttribute(description = "Number of reloads failed") public int getFailedCounter() { return failed; } @ManagedOperation(description = "Reset counters") public void resetCounters() { succeeded = 0; failed = 0; } @Override protected void doStart() throws Exception { // noop } @Override protected void doStop() throws Exception { cache.clear(); } /** * To keep state of last reloaded resource */ private static final class ResourceState { private final String name; private final Document dom; private final String xml; ResourceState(String name, Document dom, String xml) { this.name = name; this.dom = dom; this.xml = xml; } public String getName() { return name; } public Document getDom() { return dom; } public String getXml() { return xml; } } }