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.support;
018
019import java.io.ByteArrayInputStream;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024
025import org.w3c.dom.Document;
026import org.w3c.dom.Node;
027import org.w3c.dom.NodeList;
028
029import org.apache.camel.CamelContext;
030import org.apache.camel.api.management.ManagedAttribute;
031import org.apache.camel.api.management.ManagedOperation;
032import org.apache.camel.model.ModelHelper;
033import org.apache.camel.model.RouteDefinition;
034import org.apache.camel.model.RoutesDefinition;
035import org.apache.camel.spi.ReloadStrategy;
036import org.apache.camel.util.CollectionStringBuffer;
037import org.apache.camel.util.LRUCache;
038import org.apache.camel.util.ObjectHelper;
039import org.apache.camel.util.StringHelper;
040import org.apache.camel.util.XmlLineNumberParser;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044/**
045 * Base class for implementing custom {@link ReloadStrategy} SPI plugins.
046 */
047public abstract class ReloadStrategySupport extends ServiceSupport implements ReloadStrategy {
048    protected final Logger log = LoggerFactory.getLogger(getClass());
049    private CamelContext camelContext;
050
051    // store state
052    private final Map<String, ResourceState> cache = new LRUCache<String, ResourceState>(100);
053
054    private int succeeded;
055    private int failed;
056
057    @Override
058    public CamelContext getCamelContext() {
059        return camelContext;
060    }
061
062    @Override
063    public void setCamelContext(CamelContext camelContext) {
064        this.camelContext = camelContext;
065    }
066
067    @Override
068    public void onReloadXml(CamelContext camelContext, String name, InputStream resource) {
069        log.debug("Reloading routes from XML resource: {}", name);
070
071        Document dom;
072        String xml;
073        try {
074            xml = camelContext.getTypeConverter().mandatoryConvertTo(String.class, resource);
075            // the JAXB model expects the spring namespace (even for blueprint)
076            dom = XmlLineNumberParser.parseXml(new ByteArrayInputStream(xml.getBytes()), null, "camelContext,routeContext,routes", "http://camel.apache.org/schema/spring");
077        } catch (Exception e) {
078            failed++;
079            log.warn("Cannot load the resource " + name + " as XML");
080            return;
081        }
082
083        ResourceState state = cache.get(name);
084        if (state == null) {
085            state = new ResourceState(name, dom, xml);
086            cache.put(name, state);
087        }
088
089        String oldXml = state.getXml();
090        List<Integer> changed = StringHelper.changedLines(oldXml, xml);
091
092        // find all <route> which are the routes
093        NodeList list = dom.getElementsByTagName("route");
094
095        // collect which routes are updated/skipped
096        List<RouteDefinition> routes = new ArrayList<>();
097
098        if (list != null && list.getLength() > 0) {
099            for (int i = 0; i < list.getLength(); i++) {
100                Node node = list.item(i);
101
102                // what line number are this within
103                String lineNumber = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER);
104                String lineNumberEnd = (String) node.getUserData(XmlLineNumberParser.LINE_NUMBER_END);
105                if (lineNumber != null && lineNumberEnd != null && !changed.isEmpty()) {
106                    int start = Integer.valueOf(lineNumber);
107                    int end = Integer.valueOf(lineNumberEnd);
108
109                    boolean within = withinChanged(start, end, changed);
110                    if (within) {
111                        log.debug("Updating route in lines: {}-{}", start, end);
112                    } else {
113                        log.debug("No changes to route in lines: {}-{}", start, end);
114                        continue;
115                    }
116                }
117
118                try {
119                    // load from XML -> JAXB model and store as routes to be updated
120                    RoutesDefinition loaded = ModelHelper.loadRoutesDefinition(camelContext, node);
121                    if (!loaded.getRoutes().isEmpty()) {
122                        routes.addAll(loaded.getRoutes());
123                    }
124                } catch (Exception e) {
125                    failed++;
126                    throw ObjectHelper.wrapRuntimeCamelException(e);
127                }
128            }
129        }
130
131        if (!routes.isEmpty()) {
132            try {
133                boolean unassignedRouteIds = false;
134
135                CollectionStringBuffer csb = new CollectionStringBuffer(",");
136                // collect route ids and force assign ids if not in use
137                for (RouteDefinition route : routes) {
138                    unassignedRouteIds |= !route.hasCustomIdAssigned();
139                    String id = route.idOrCreate(camelContext.getNodeIdFactory());
140                    csb.append(id);
141                }
142                log.debug("Reloading routes: [{}] from XML resource: {}", csb, name);
143
144                if (unassignedRouteIds) {
145                    log.warn("Routes with no id's detected. Its recommended to assign id's to your routes so Camel can reload the routes correctly.");
146                }
147                // update the routes (add will remove and shutdown first)
148                camelContext.addRouteDefinitions(routes);
149
150                log.info("Reloaded routes: [{}] from XML resource: {}", csb, name);
151            } catch (Exception e) {
152                failed++;
153                throw ObjectHelper.wrapRuntimeCamelException(e);
154            }
155
156        }
157
158        // update cache
159        state = new ResourceState(name, dom, xml);
160        cache.put(name, state);
161
162        succeeded++;
163    }
164
165    private boolean withinChanged(int start, int end, List<Integer> changed) {
166        for (int change : changed) {
167            log.trace("Changed line: {} within {}-{}", change, start, end);
168            if (change >= start && change <= end) {
169                return true;
170            }
171        }
172        return false;
173    }
174
175    @ManagedAttribute(description = "Number of reloads succeeded")
176    public int getReloadCounter() {
177        return succeeded;
178    }
179
180    @ManagedAttribute(description = "Number of reloads failed")
181    public int getFailedCounter() {
182        return failed;
183    }
184
185    @ManagedOperation(description = "Reset counters")
186    public void resetCounters() {
187        succeeded = 0;
188        failed = 0;
189    }
190
191    @Override
192    protected void doStart() throws Exception {
193        // noop
194    }
195
196    @Override
197    protected void doStop() throws Exception {
198        cache.clear();
199    }
200
201    /**
202     * To keep state of last reloaded resource
203     */
204    private static final class ResourceState {
205        private final String name;
206        private final Document dom;
207        private final String xml;
208
209        ResourceState(String name, Document dom, String xml) {
210            this.name = name;
211            this.dom = dom;
212            this.xml = xml;
213        }
214
215        public String getName() {
216            return name;
217        }
218
219        public Document getDom() {
220            return dom;
221        }
222
223        public String getXml() {
224            return xml;
225        }
226    }
227
228}