diff --git a/components/camel-xslt/src/main/docs/xslt-component.adoc b/components/camel-xslt/src/main/docs/xslt-component.adoc index 85cfc8f6f17b6..18bbad4fea25c 100644 --- a/components/camel-xslt/src/main/docs/xslt-component.adoc +++ b/components/camel-xslt/src/main/docs/xslt-component.adoc @@ -185,6 +185,14 @@ TIP: You can set `contentCache=false` and refer to a non-existing template, such as this will tell Camel to not load `dummy.xsl` on startup but to load the stylesheet on demand. And because you provide the stylesheet via headers, then it is fully dynamic. +== Live reload in dev mode + +When routes-reload is enabled (`camel.main.routesReloadEnabled=true`, automatically set by `camel run --dev`), +Camel disables `contentCache` on the XSLT component so that edits to the stylesheet file are picked up +on the next message without having to restart the route. User properties (e.g. +`camel.component.xslt.contentCache=true`) and explicit endpoint/URI settings are always respected: +set `contentCache=true` to keep caching even in dev mode. + == Accessing warnings, errors and fatalErrors from XSLT ErrorListener Any warning/error or fatalError is stored on diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java index a7b580fe897ad..84793d6100f30 100644 --- a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java +++ b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java @@ -298,6 +298,9 @@ public static void configure(CamelContext camelContext, DefaultConfigurationProp reloader.setPattern(config.getRoutesReloadPattern()); reloader.setRemoveAllRoutes(config.isRoutesReloadRemoveAllRoutes()); camelContext.addService(reloader); + // disable contentCache on resource-based components so that resource files (e.g. XSLT + // stylesheets, templates) are reloaded live without restarting routes + camelContext.addLifecycleStrategy(new DevModeContentCacheStrategy()); } if (config.getDumpRoutes() != null) { DumpRoutesStrategy drs = camelContext.getCamelContextExtension().getContextPlugin(DumpRoutesStrategy.class); diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DevModeContentCacheStrategy.java b/core/camel-main/src/main/java/org/apache/camel/main/DevModeContentCacheStrategy.java new file mode 100644 index 0000000000000..8dc4ebde5fb97 --- /dev/null +++ b/core/camel-main/src/main/java/org/apache/camel/main/DevModeContentCacheStrategy.java @@ -0,0 +1,53 @@ +/* + * 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.main; + +import org.apache.camel.Component; +import org.apache.camel.spi.PropertyConfigurer; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.support.LifecycleStrategySupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Lifecycle strategy that disables {@code contentCache} on resource-based components when routes-reload is enabled, so + * users can edit a resource file (e.g. an XSLT stylesheet, FreeMarker template) and see the change applied live. + * + * Components are detected via their generated {@link PropertyConfigurer}: any component whose configurer exposes a + * {@code contentCache} option currently evaluating to {@link Boolean#TRUE} is flipped to {@code false}. User properties + * applied later (e.g. {@code camel.component..contentCache=true}) will override this default. + */ +class DevModeContentCacheStrategy extends LifecycleStrategySupport { + + private static final Logger LOG = LoggerFactory.getLogger(DevModeContentCacheStrategy.class); + + private static final String CONTENT_CACHE = "contentCache"; + + @Override + public void onComponentAdd(String name, Component component) { + PropertyConfigurer configurer = component.getComponentPropertyConfigurer(); + if (!(configurer instanceof PropertyConfigurerGetter getter)) { + return; + } + Object value = getter.getOptionValue(component, CONTENT_CACHE, true); + if (Boolean.TRUE.equals(value) + && configurer.configure(component.getCamelContext(), component, CONTENT_CACHE, Boolean.FALSE, true)) { + LOG.debug("Routes-reload is enabled: disabling contentCache on component '{}' for live resource reload", + name); + } + } +} diff --git a/core/camel-main/src/test/java/org/apache/camel/main/MainDevModeContentCacheTest.java b/core/camel-main/src/test/java/org/apache/camel/main/MainDevModeContentCacheTest.java new file mode 100644 index 0000000000000..dd97a841fcccb --- /dev/null +++ b/core/camel-main/src/test/java/org/apache/camel/main/MainDevModeContentCacheTest.java @@ -0,0 +1,204 @@ +/* + * 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.main; + +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Consumer; +import org.apache.camel.Endpoint; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.spi.PropertyConfigurer; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.support.DefaultComponent; +import org.apache.camel.support.DefaultEndpoint; +import org.apache.camel.support.component.PropertyConfigurerSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class MainDevModeContentCacheTest { + + @Test + public void shouldDisableContentCacheWhenRoutesReloadEnabled() { + Main main = new Main(); + main.configure().withRoutesReloadEnabled(true); + main.bind("dummy", new TestContentCacheComponent()); + + main.start(); + try { + TestContentCacheComponent c = main.getCamelContext().getComponent("dummy", TestContentCacheComponent.class); + assertFalse(c.isContentCache(), + "contentCache should be auto-disabled when routesReloadEnabled=true"); + } finally { + main.stop(); + } + } + + @Test + public void shouldKeepContentCacheEnabledWhenRoutesReloadDisabled() { + Main main = new Main(); + // routesReloadEnabled defaults to false + main.bind("dummy", new TestContentCacheComponent()); + + main.start(); + try { + TestContentCacheComponent c = main.getCamelContext().getComponent("dummy", TestContentCacheComponent.class); + assertTrue(c.isContentCache(), + "contentCache should remain at its default (true) when routesReloadEnabled is not active"); + } finally { + main.stop(); + } + } + + @Test + public void shouldRespectExplicitContentCacheFalse() { + Main main = new Main(); + main.configure().withRoutesReloadEnabled(true); + TestContentCacheComponent component = new TestContentCacheComponent(); + component.setContentCache(false); + main.bind("dummy", component); + + main.start(); + try { + TestContentCacheComponent c = main.getCamelContext().getComponent("dummy", TestContentCacheComponent.class); + assertFalse(c.isContentCache(), + "explicit user setting (false) must not be touched by dev-mode auto-flip"); + } finally { + main.stop(); + } + } + + @Test + public void shouldHonorMainPropertyOverride() { + Main main = new Main(); + main.configure().withRoutesReloadEnabled(true); + main.addInitialProperty("camel.component.dummy.contentCache", "true"); + main.bind("dummy", new TestContentCacheComponent()); + + main.start(); + try { + TestContentCacheComponent c = main.getCamelContext().getComponent("dummy", TestContentCacheComponent.class); + assertTrue(c.isContentCache(), + "camel.component..contentCache=true must override the dev-mode auto-flip"); + } finally { + main.stop(); + } + } + + @Test + public void shouldRespectExplicitContentCacheOnUri() { + Main main = new Main(); + main.configure().withRoutesReloadEnabled(true); + main.bind("dummy", new TestContentCacheComponent()); + + main.start(); + try { + TestContentCacheComponent c = main.getCamelContext().getComponent("dummy", TestContentCacheComponent.class); + assertFalse(c.isContentCache(), "component-level contentCache should be auto-disabled"); + + TestContentCacheEndpoint endpoint + = (TestContentCacheEndpoint) main.getCamelContext().getEndpoint("dummy:foo?contentCache=true"); + assertTrue(endpoint.isContentCache(), + "explicit contentCache=true on URI must override the component-level auto-flip"); + } finally { + main.stop(); + } + } + + static final class TestContentCacheComponent extends DefaultComponent { + + private boolean contentCache = true; + + public boolean isContentCache() { + return contentCache; + } + + public void setContentCache(boolean contentCache) { + this.contentCache = contentCache; + } + + @Override + public PropertyConfigurer getComponentPropertyConfigurer() { + return new TestContentCacheComponentConfigurer(); + } + + @Override + protected Endpoint createEndpoint(String uri, String remaining, Map parameters) throws Exception { + TestContentCacheEndpoint endpoint = new TestContentCacheEndpoint(uri, this); + endpoint.setContentCache(contentCache); + setProperties(endpoint, parameters); + return endpoint; + } + } + + static final class TestContentCacheComponentConfigurer + extends PropertyConfigurerSupport + implements PropertyConfigurer, PropertyConfigurerGetter { + + @Override + public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) { + if ("contentcache".equalsIgnoreCase(name)) { + ((TestContentCacheComponent) target).setContentCache(property(camelContext, boolean.class, value)); + return true; + } + return false; + } + + @Override + public Object getOptionValue(Object target, String name, boolean ignoreCase) { + if ("contentcache".equalsIgnoreCase(name)) { + return ((TestContentCacheComponent) target).isContentCache(); + } + return null; + } + + @Override + public Class getOptionType(String name, boolean ignoreCase) { + return "contentcache".equalsIgnoreCase(name) ? boolean.class : null; + } + } + + static final class TestContentCacheEndpoint extends DefaultEndpoint { + + private boolean contentCache; + + TestContentCacheEndpoint(String uri, TestContentCacheComponent component) { + super(uri, component); + } + + public boolean isContentCache() { + return contentCache; + } + + public void setContentCache(boolean contentCache) { + this.contentCache = contentCache; + } + + @Override + public Producer createProducer() { + throw new UnsupportedOperationException(); + } + + @Override + public Consumer createConsumer(Processor processor) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc index d1e3ccb2cc161..ffd96ad7aad7f 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc @@ -46,6 +46,12 @@ and dev consoles for nodes inside Choice EIP branches. The `camel wrapper` command now installs the scripts as `camel` instead of `camelw`. You can use the `--command-name=camelw` to use the old name. +When `camel.main.routesReloadEnabled=true` (automatically set by `camel run --dev`), Camel now +auto-disables `contentCache` on resource-based components (such as `xslt`) whose default is +`true`, so that edits to the resource file are picked up on the next message without restarting +the route. Set `camel.component..contentCache=true` (or pass `?contentCache=true` on the +URI) to opt back in to caching during dev mode. + === camel-yaml-dsl A new canonical JSON Schema variant (`camelYamlDsl-canonical.json`) has been added alongside the existing classic diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index de578f12c467a..0fc137256bcc8 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -529,6 +529,16 @@ and reloaded. You can also delete files to remove routes. NOTE: You cannot use both files and source dir together. The following is not allowed: `camel run abc.java --source-dir=mycode`. +==== Live reload of resource files + +When `--dev` is used, Camel also disables `contentCache` on resource-based components (such as +`xslt`) whose default is `true`, so that edits to resource files are picked up on the next message +without having to restart the route. + +This is driven by `camel.main.routesReloadEnabled` (set automatically by `--dev`). User properties +(e.g. `camel.component.xslt.contentCache=true`) and explicit endpoint/URI settings are always +respected: set `contentCache=true` to keep caching even in dev mode. + ==== Loading new routes into existing Camel *Available as of Camel 4.17* diff --git a/docs/user-manual/modules/ROOT/pages/route-reload.adoc b/docs/user-manual/modules/ROOT/pages/route-reload.adoc index 03f2c7e745dd5..ad80593a7b25b 100644 --- a/docs/user-manual/modules/ROOT/pages/route-reload.adoc +++ b/docs/user-manual/modules/ROOT/pages/route-reload.adoc @@ -70,6 +70,16 @@ This is necessary because Apache Camel must stop the existing routes from runnin And adding new routes is therefore possible as they would have a new unique route id specified. +=== Live reload of resource files + +In addition to reloading routes, when `routesReloadEnabled=true` Camel also disables the +`contentCache` option on resource-based components (such as `xslt`) whose default is `true`. This +way, edits to resource files are picked up on the next message without having to restart the +route. + +User properties (e.g. `camel.component.xslt.contentCache=true`) and explicit endpoint/URI settings +are always respected: set `contentCache=true` to keep caching even when route reload is enabled. + == See Also See related xref:context-reload.adoc[].