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.LRUCacheFactory;
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
050    protected Map<String, Object> cache;
051
052    private CamelContext camelContext;
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 {} as XML", name);
080            return;
081        }
082
083        ResourceState state = ObjectHelper.cast(ResourceState.class, 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    public void setSucceeded(int succeeded) {
186        this.succeeded = succeeded;
187    }
188    
189    public void setFailed(int failed) {
190        this.failed = failed;
191    }
192
193    @ManagedOperation(description = "Reset counters")
194    public void resetCounters() {
195        succeeded = 0;
196        failed = 0;
197    }
198
199    @Override
200    @SuppressWarnings("unchecked")
201    protected void doStart() throws Exception {
202        // noop
203        cache = LRUCacheFactory.newLRUCache(100);
204    }
205
206    @Override
207    protected void doStop() throws Exception {
208        cache.clear();
209    }
210
211    /**
212     * To keep state of last reloaded resource
213     */
214    private static final class ResourceState {
215        private final String name;
216        private final Document dom;
217        private final String xml;
218
219        ResourceState(String name, Document dom, String xml) {
220            this.name = name;
221            this.dom = dom;
222            this.xml = xml;
223        }
224
225        public String getName() {
226            return name;
227        }
228
229        public Document getDom() {
230            return dom;
231        }
232
233        public String getXml() {
234            return xml;
235        }
236    }
237
238}