All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.support.RouteWatcherReloadStrategy Maven / Gradle / Ivy

The newest version!
/*
 * 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.File;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;

import org.apache.camel.Route;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.ServiceStatus;
import org.apache.camel.StartupSummaryLevel;
import org.apache.camel.spi.PropertiesComponent;
import org.apache.camel.spi.PropertiesReload;
import org.apache.camel.spi.PropertiesSource;
import org.apache.camel.spi.Resource;
import org.apache.camel.util.AntPathMatcher;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.OrderedLocationProperties;
import org.apache.camel.util.OrderedProperties;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Watcher strategy for triggering reloading of Camel routes in a running Camel application. The strategy watches a
 * directory (and subdirectories) for file changes. By default, the strategy is matching Camel routes in XML or YAML
 * files.
 */
public class RouteWatcherReloadStrategy extends FileWatcherResourceReloadStrategy {

    /**
     * Special when reloading routes(s) requires to also ensure other resources are reloaded together such as
     * camel-java-joor-dsl to ensure all resources are compiled in the same compilation unit.
     */
    public static final String RELOAD_RESOURCES = "RouteWatcherReloadResources";

    private static final Logger LOG = LoggerFactory.getLogger(RouteWatcherReloadStrategy.class);

    private static final String DEFAULT_PATTERN = "*.yaml,*.xml";

    private String pattern;
    private boolean removeAllRoutes = true;
    private final List previousSources = new ArrayList<>();

    public RouteWatcherReloadStrategy() {
    }

    public RouteWatcherReloadStrategy(String directory) {
        this(directory, false);
    }

    public RouteWatcherReloadStrategy(String directory, boolean recursive) {
        super(directory, recursive);
    }

    public String getPattern() {
        return pattern;
    }

    /**
     * Used for inclusive filtering of routes from directories.
     *
     * Typical used for specifying to accept routes in XML or YAML files, such as *.yaml,*.xml. Multiple
     * patterns can be specified separated by comma.
     */
    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    public boolean isRemoveAllRoutes() {
        return removeAllRoutes;
    }

    /**
     * When reloading routes should all existing routes be stopped and removed.
     *
     * By default, Camel will stop and remove all existing routes before reloading routes. This ensures that only the
     * reloaded routes will be active. If disabled then only routes with the same route id is updated, and any existing
     * routes are continued to run.
     */
    public void setRemoveAllRoutes(boolean removeAllRoutes) {
        this.removeAllRoutes = removeAllRoutes;
    }

    @Override
    protected void doStart() throws Exception {
        ObjectHelper.notNull(getFolder(), "folder", this);

        if (pattern == null || pattern.isBlank()) {
            pattern = DEFAULT_PATTERN;
        } else if ("*".equals(pattern)) {
            pattern = "**"; // use ant style matching to match everything
        }

        final String base = new File(getFolder()).getAbsolutePath();
        final AntPathMatcher matcher = new AntPathMatcher();

        if (getFileFilter() == null) {
            // file matcher that matches via the ant path matcher
            final String[] parts = pattern.split(",");
            setFileFilter(f -> {
                for (String part : parts) {
                    // strip starting directory, so we have a relative name to the starting folder
                    String path = f.getAbsolutePath();
                    if (path.startsWith(base)) {
                        path = FileUtil.stripPath(path);
                    }

                    String name = FileUtil.compactPath(f.getPath());
                    boolean exact = name.equals(part);
                    boolean result = exact || matcher.match(part, path, false);
                    LOG.trace("Accepting file pattern:{} path:{} -> {}", part, path, result);

                    if (result) {
                        return true;
                    }
                }
                return false;
            });
        }

        if (getResourceReload() == null) {
            // attach listener that triggers the route update
            setResourceReload((name, resource) -> {
                if (name.endsWith(".properties")) {
                    onPropertiesReload(resource, true);
                } else {
                    onRouteReload(List.of(resource), false);
                }
            });
        }

        super.doStart();
    }

    @Override
    protected String startupMessage(File dir) {
        return "Live route reloading enabled (directory: " + dir + ")";
    }

    protected boolean onPropertiesReload(Resource resource, boolean reloadRoutes) throws Exception {
        // optimize to only update if something changed
        OrderedLocationProperties changed = null;

        PropertiesComponent pc = getCamelContext().getPropertiesComponent();
        PropertiesReload pr = getCamelContext().hasService(PropertiesReload.class);
        if (pr != null) {
            // load the properties, so we can update (remember location)
            InputStream is = resource.getInputStream();
            OrderedProperties tmp = new OrderedProperties();
            tmp.load(is);
            IOHelper.close(is);
            changed = new OrderedLocationProperties();
            changed.putAll(resource.getLocation(), tmp);
            // filter to only keep changed properties
            pc.keepOnlyChangeProperties(changed);
        }

        boolean reloaded = false;
        if (changed != null && !changed.isEmpty()) {
            LOG.info("Reloading properties: {}. (Only Camel routes and components can be updated with changes)",
                    resource.getLocation());
            reloaded = pc.reloadProperties(resource.getLocation());
            if (reloaded) {
                pr.onReload(resource.getLocation(), changed);
                // trigger all routes to be reloaded
                if (reloadRoutes) {
                    onRouteReload(null, false);
                }
            } else {
                // this may be a new properties file, so we need to add as new known location
                String existing = getPropertiesByLocation(resource.getLocation());
                if (existing == null) {
                    // remove scheme
                    String loc = resource.getLocation();
                    if (loc.contains(":")) {
                        loc = StringHelper.after(loc, ":");
                    }
                    PropertiesSource ps = pc.getPropertiesSourceFactory().newFilePropertiesSource(loc);
                    pc.addPropertiesSource(ps);
                    reloaded = true;
                }
            }
        }
        return reloaded;
    }

    private String getPropertiesByLocation(String loc) {
        PropertiesComponent pc = getCamelContext().getPropertiesComponent();
        for (String s : pc.getLocations()) {
            if (s.endsWith(";optional=true")) {
                s = s.substring(0, s.length() - 14);
            }
            if (Objects.equals(s, loc)) {
                return loc;
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    protected void onRouteReload(Collection resources, boolean removeEverything) {
        // remember all existing resources
        List sources = new ArrayList<>();

        if (!previousSources.isEmpty()) {
            // last update failed, so we need to update all previous sources to ensure we go back
            // to the last working set
            previousSources.forEach(rs -> {
                // remember all the sources of the current routes (except the updated)
                if (rs != null && !equalResourceLocation(resources, rs)) {
                    sources.add(rs);
                }
            });
        }

        try {
            // should all existing routes be stopped and removed first?
            if (removeAllRoutes) {
                // remember all the sources of the current routes (except the updated)
                getCamelContext().getRoutes().forEach(r -> {
                    Resource rs = r.getSourceResource();
                    if (rs != null && !equalResourceLocation(resources, rs)) {
                        sources.add(rs);
                    }
                });
                // first stop and remove all routes
                getCamelContext().getRouteController().removeAllRoutes();
                // remove left-over route templates and endpoints, so we can start on a fresh
                getCamelContext().removeRouteTemplates("*");
                getCamelContext().getEndpointRegistry().clear();
            }

            if (resources != null) {
                for (Resource resource : resources) {
                    if (Files.exists(Paths.get(resource.getURI()))) {
                        sources.add(resource);
                    }
                }
            }

            Collection extras
                    = getCamelContext().getRegistry().lookupByNameAndType(RELOAD_RESOURCES, Collection.class);
            if (extras != null) {
                for (Resource extra : extras) {
                    if (!sources.contains(extra)) {
                        sources.add(extra);
                    }
                }
            }

            // just in case remember this set of sources as what was attempted previously to update
            // in case the update fails with an exception
            previousSources.clear();
            previousSources.addAll(sources);

            // special situation where we remove all routes
            if (removeEverything) {
                sources.clear();
            }

            // reload those other routes that was stopped and removed as we want to keep running those
            Set ids
                    = PluginHelper.getRoutesLoader(getCamelContext()).updateRoutes(sources);

            // update okay, so clear as we do not need to remember those anymore
            previousSources.clear();

            if (!ids.isEmpty()) {
                List lines = new ArrayList<>();
                int total = 0;
                int started = 0;
                for (String id : ids) {
                    total++;
                    Route route = getCamelContext().getRoute(id);
                    if (route != null) {
                        ServiceStatus status = getCamelContext().getRouteController().getRouteStatus(id);
                        if (status == null) {
                            // undefined status of route (should not really happen)
                            LOG.warn("Cannot get route status for route: {}. This route is skipped.", id);
                            continue;
                        }
                        if (ServiceStatus.Started.equals(status)) {
                            started++;
                        }
                        // use basic endpoint uri to not log verbose details or potential sensitive data
                        String uri = route.getEndpoint().getEndpointBaseUri();
                        uri = URISupport.sanitizeUri(uri);
                        String loc = route.getSourceLocationShort();
                        if (loc == null) {
                            loc = "";
                        }
                        lines.add(String.format("    %s %s (%s) (source: %s)", status, id, uri, loc));
                    }
                }
                LOG.info("Routes reloaded summary (total:{} started:{})", total, started);
                // if we are default/verbose then log each route line
                if (getCamelContext().getStartupSummaryLevel() == StartupSummaryLevel.Default
                        || getCamelContext().getStartupSummaryLevel() == StartupSummaryLevel.Verbose) {
                    for (String line : lines) {
                        LOG.info(line);
                    }
                }
            }

            // fire events for routes reloaded
            int index = 1;
            int total = ids.size();
            for (String id : ids) {
                Route route = getCamelContext().getRoute(id);
                if (route != null) {
                    EventHelper.notifyRouteReloaded(getCamelContext(), route, index++, total);
                }
            }

            if (!removeAllRoutes) {
                // if not all previous routes are removed then to have safe route reloading
                // it is recommended to configure ids on the routes
                StringJoiner sj = new StringJoiner("\n    ");
                for (String id : ids) {
                    Route route = getCamelContext().getRoute(id);
                    if (route != null && !route.isCustomId()) {
                        sj.add(route.getEndpoint().getEndpointUri());
                    }
                }
                if (sj.length() > 0) {
                    LOG.warn(
                            "Routes with no id's detected. Its recommended to assign route id's to your routes so Camel can reload the routes correctly.\n    Unassigned routes:\n    {}",
                            sj);
                }
            }
        } catch (Exception e) {
            throw RuntimeCamelException.wrapRuntimeException(e);
        }
    }

    /**
     * Whether the target is loading any of the given sources
     */
    private static boolean equalResourceLocation(Collection sources, Resource target) {
        if (sources == null || target == null || sources.isEmpty()) {
            return false;
        }
        for (Resource source : sources) {
            if (equalResourceLocation(source, target)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Whether the two resources are loading the same resource
     */
    private static boolean equalResourceLocation(Resource source, Resource target) {
        if (source == null || target == null) {
            return false;
        }

        // use URI to match as file/classpath resources may refer to the same uri
        URI u1 = source.getURI();
        URI u2 = target.getURI();
        boolean answer = u1.equals(u2);
        if (!answer) {
            // file and classpath may refer to the same when they have src/main/resources && target/classes
            String s1 = u1.toString().replace("src/main/resources/", "").replace("src/test/resources/", "")
                    .replace("target/classes/", "");
            String s2 = u2.toString().replace("src/main/resources/", "").replace("src/test/resources/", "")
                    .replace("target/classes/", "");
            answer = s1.equals(s2);
        }
        return answer;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy