This is an automated email from the ASF dual-hosted git repository.

jayblanc pushed a commit to branch UNOMI-973-file-endpoint-containment
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 3c7f42b681ce68884dc0ea8696cc6754f1179232
Author: Jérôme Blanchard <[email protected]>
AuthorDate: Tue Aug 11 16:20:10 2026 +0200

    UNOMI-973: Record on a configuration whether its endpoint can be honoured
    
    The permitted directories are an operational setting and the configurations
    are user data, so the two drift apart: a configuration that was legitimate
    when it was created is refused once the deployment is reconfigured. Refusing
    it silently left its owner with a configuration that looks fine and does
    nothing, the only trace being a log line.
    
    A configuration whose endpoint is refused while its route is built is now
    marked INVALID_ENDPOINT and saved, so it can be seen and dealt with. Nothing
    is deleted and no restart is blocked: correcting or removing it belongs to
    whoever owns it. Restoring the permitted directories clears the mark on its
    own at the next rebuild, so operations can undo a change without anyone
    touching the configurations.
    
    The status is its own value rather than one of the execution statuses: those
    report on a run that happened, this one says no run can. Keeping them apart
    is what makes the mark safe to clear -- the record of a run that genuinely
    failed is left alone.
    
    Saving is done without asking for a route refresh, which would rebuild the
    route, refuse it again and save it again, without end.
---
 .../apache/unomi/router/api/RouterConstants.java   |   7 +
 .../router/core/context/RouterCamelContext.java    |   2 +
 .../route/ProfileExportCollectRouteBuilder.java    |   7 +
 .../route/ProfileImportFromSourceRouteBuilder.java |   1 +
 .../core/route/RouterAbstractRouteBuilder.java     |  33 +++
 .../core/route/FileEndpointContainmentTest.java    |  15 +
 .../core/route/RefusedConfigurationStatusTest.java | 305 +++++++++++++++++++++
 7 files changed, 370 insertions(+)

diff --git 
a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
 
b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
index 03d84bcb2..9df7a2752 100644
--- 
a/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
+++ 
b/extensions/router/router-api/src/main/java/org/apache/unomi/router/api/RouterConstants.java
@@ -32,6 +32,13 @@ public interface RouterConstants {
     String CONFIG_STATUS_COMPLETE_ERRORS = "ERRORS";
     String CONFIG_STATUS_COMPLETE_SUCCESS = "SUCCESS";
     String CONFIG_STATUS_COMPLETE_WITH_ERRORS = "WITH_ERRORS";
+    /**
+     * The configuration names an endpoint that cannot be honoured, so no 
route carries it. Kept apart
+     * from the execution statuses above: those report on a run that happened, 
this one says no run can.
+     * It is set and cleared by the route builders alone, so that restoring 
the deployment's permitted
+     * directories brings the configuration back on its own.
+     */
+    String CONFIG_STATUS_INVALID_ENDPOINT = "INVALID_ENDPOINT";
 
     String IMPORT_EXPORT_CONFIG_TYPE_RECURRENT = "recurrent";
     String IMPORT_EXPORT_CONFIG_TYPE_ONESHOT = "oneshot";
diff --git 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
index 15f401880..d0e8dcdae 100644
--- 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
+++ 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
@@ -211,6 +211,7 @@ public class RouterCamelContext implements 
IRouterCamelContext {
         //Profiles collect
         ProfileExportCollectRouteBuilder profileExportCollectRouteBuilder = 
new ProfileExportCollectRouteBuilder(kafkaProps, configType);
         
profileExportCollectRouteBuilder.setExportConfigurationList(exportConfigurationService.getAll());
+        
profileExportCollectRouteBuilder.setExportConfigurationService(exportConfigurationService);
         
profileExportCollectRouteBuilder.setPersistenceService(persistenceService);
         profileExportCollectRouteBuilder.setAllowedEndpoints(allowedEndpoints);
         
profileExportCollectRouteBuilder.setPermittedExportBaseDirs(permittedExportBaseDirs);
@@ -276,6 +277,7 @@ public class RouterCamelContext implements 
IRouterCamelContext {
         if 
(RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(exportConfiguration.getConfigType()))
 {
             ProfileExportCollectRouteBuilder profileExportCollectRouteBuilder 
= new ProfileExportCollectRouteBuilder(kafkaProps, configType);
             
profileExportCollectRouteBuilder.setExportConfigurationList(Collections.singletonList(exportConfiguration));
+            
profileExportCollectRouteBuilder.setExportConfigurationService(exportConfigurationService);
             
profileExportCollectRouteBuilder.setPersistenceService(persistenceService);
             
profileExportCollectRouteBuilder.setAllowedEndpoints(allowedEndpoints);
             
profileExportCollectRouteBuilder.setPermittedExportBaseDirs(permittedExportBaseDirs);
diff --git 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
index d0f075f1c..f59b46edb 100644
--- 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
+++ 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileExportCollectRouteBuilder.java
@@ -23,6 +23,7 @@ import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.EndpointValidator;
 import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.RouterConstants;
+import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 import org.apache.unomi.router.core.bean.CollectProfileBean;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -38,6 +39,7 @@ public class ProfileExportCollectRouteBuilder extends 
RouterAbstractRouteBuilder
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ProfileExportCollectRouteBuilder.class);
 
     private List<ExportConfiguration> exportConfigurationList;
+    private ImportExportConfigurationService<ExportConfiguration> 
exportConfigurationService;
     private PersistenceService persistenceService;
 
     public ProfileExportCollectRouteBuilder(Map<String, String> kafkaProps, 
String configType) {
@@ -63,6 +65,7 @@ public class ProfileExportCollectRouteBuilder extends 
RouterAbstractRouteBuilder
                 if ((Map<String, String>) 
exportConfiguration.getProperties().get("mapping") != null) {
                     String destinationEndpoint = (String) 
exportConfiguration.getProperties().get("destination");
                     String refusal = 
EndpointValidator.validate(destinationEndpoint, allowedEndpoints, 
permittedBaseDirs);
+                    recordEndpointOutcome(exportConfiguration, 
exportConfigurationService, refusal);
                     if (refusal == null) {
                         String timerString = 
"timer://collectProfile?fixedRate=true&period=" + (String) 
exportConfiguration.getProperties().get("period");
                         if ((String) 
exportConfiguration.getProperties().get("delay") != null) {
@@ -103,6 +106,10 @@ public class ProfileExportCollectRouteBuilder extends 
RouterAbstractRouteBuilder
         this.permittedBaseDirs = permittedExportBaseDirs;
     }
 
+    public void 
setExportConfigurationService(ImportExportConfigurationService<ExportConfiguration>
 exportConfigurationService) {
+        this.exportConfigurationService = exportConfigurationService;
+    }
+
     public void setExportConfigurationList(List<ExportConfiguration> 
exportConfigurationList) {
         this.exportConfigurationList = exportConfigurationList;
     }
diff --git 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java
 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java
index 54b9109fc..4c468a659 100644
--- 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java
+++ 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/ProfileImportFromSourceRouteBuilder.java
@@ -98,6 +98,7 @@ public class ProfileImportFromSourceRouteBuilder extends 
RouterAbstractRouteBuil
                 }
 
                 String refusal = EndpointValidator.validate(endpoint, 
allowedEndpoints, permittedBaseDirs);
+                recordEndpointOutcome(importConfiguration, 
importConfigurationService, refusal);
                 if (refusal == null) {
                     ProcessorDefinition prDef = from(endpoint)
                             .routeId(importConfiguration.getItemId())// This 
allow identification of the route for manual start/stop
diff --git 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java
 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java
index 00ae70204..fa4998998 100644
--- 
a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java
+++ 
b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/route/RouterAbstractRouteBuilder.java
@@ -23,7 +23,9 @@ import org.apache.camel.component.kafka.KafkaConfiguration;
 import org.apache.camel.component.kafka.KafkaEndpoint;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.router.api.ImportExportConfiguration;
 import org.apache.unomi.router.api.RouterConstants;
+import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 
 import java.util.Map;
 
@@ -61,6 +63,37 @@ public abstract class RouterAbstractRouteBuilder extends 
RouteBuilder {
         this.configType = configType;
     }
 
+    /**
+     * Records, on the configuration itself, whether the endpoint it names can 
be honoured.
+     *
+     * <p>The permitted directories are an operational setting and the 
configurations are user data, so
+     * the two drift apart: a configuration that was legitimate when it was 
created can be refused after
+     * the deployment is reconfigured. Refusing it silently leaves the owner 
with a configuration that
+     * looks fine and does nothing, so the refusal is written where they will 
see it. It is theirs to
+     * correct or remove — nothing is deleted here.
+     *
+     * <p>The other way round matters just as much: restoring the permitted 
directories must bring the
+     * configuration back on its own, without anyone having to touch it. Only 
the status this method
+     * sets is cleared, so the record of a run that genuinely failed survives.
+     *
+     * <p>The configuration is saved without asking for its running route to 
be refreshed: the refresh
+     * would rebuild the route, refuse it again and save it again, without end.
+     *
+     * @param configuration the configuration whose endpoint was examined
+     * @param service       the service holding that kind of configuration
+     * @param refusal       the reason the endpoint was refused, or {@code 
null} if it can be honoured
+     */
+    protected <T extends ImportExportConfiguration> void recordEndpointOutcome(
+            T configuration, ImportExportConfigurationService<T> service, 
String refusal) {
+        if (refusal != null) {
+            
configuration.setStatus(RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT);
+            service.save(configuration, false);
+        } else if 
(RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT.equals(configuration.getStatus()))
 {
+            configuration.setStatus(null);
+            service.save(configuration, false);
+        }
+    }
+
     public Object getEndpointURI(String direction, String 
operationDepositBuffer) {
         Object endpoint;
         if (RouterConstants.CONFIG_TYPE_KAFKA.equals(configType)) {
diff --git 
a/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/FileEndpointContainmentTest.java
 
b/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/FileEndpointContainmentTest.java
index e10d3b0c6..78287c6f8 100644
--- 
a/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/FileEndpointContainmentTest.java
+++ 
b/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/FileEndpointContainmentTest.java
@@ -23,6 +23,7 @@ import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.ImportConfiguration;
 import org.apache.unomi.router.api.ProfileToImport;
 import org.apache.unomi.router.api.RouterConstants;
+import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -382,6 +383,7 @@ public class FileEndpointContainmentTest {
         ProfileImportFromSourceRouteBuilder builder =
                 new ProfileImportFromSourceRouteBuilder(NO_KAFKA, 
RouterConstants.CONFIG_TYPE_NOBROKER);
         builder.setImportConfigurationList(Arrays.asList(configurations));
+        
builder.setImportConfigurationService(discardingConfigurationService());
         builder.setProfileService(noOpProfileService());
         builder.setJacksonDataFormat(new 
JacksonDataFormat(ProfileToImport.class));
         builder.setAllowedEndpoints(allowedEndpoints);
@@ -394,6 +396,7 @@ public class FileEndpointContainmentTest {
         ProfileExportCollectRouteBuilder builder =
                 new ProfileExportCollectRouteBuilder(NO_KAFKA, 
RouterConstants.CONFIG_TYPE_NOBROKER);
         builder.setExportConfigurationList(Arrays.asList(configurations));
+        
builder.setExportConfigurationService(discardingConfigurationService());
         builder.setJacksonDataFormat(new 
JacksonDataFormat(ProfileToImport.class));
         builder.setAllowedEndpoints(DEFAULT_ALLOWED_ENDPOINTS);
         
builder.setPermittedExportBaseDirs(permittedExportDir.getAbsolutePath());
@@ -401,6 +404,18 @@ public class FileEndpointContainmentTest {
         camelContext.addRoutes(builder);
     }
 
+    /**
+     * A refused configuration is recorded, and these tests are not about 
that: what they observe is
+     * whether the route was built.
+     */
+    @SuppressWarnings("unchecked")
+    private static <T> ImportExportConfigurationService<T> 
discardingConfigurationService() {
+        return (ImportExportConfigurationService<T>) Proxy.newProxyInstance(
+                ImportExportConfigurationService.class.getClassLoader(),
+                new Class<?>[]{ImportExportConfigurationService.class},
+                (proxy, method, args) -> "save".equals(method.getName()) ? 
args[0] : null);
+    }
+
     /**
      * The route builders ask the profile service for the profile property 
types while they build.
      * Nothing in these tests depends on what it answers.
diff --git 
a/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/RefusedConfigurationStatusTest.java
 
b/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/RefusedConfigurationStatusTest.java
new file mode 100644
index 000000000..d0daf23e4
--- /dev/null
+++ 
b/extensions/router/router-core/src/test/java/org/apache/unomi/router/core/route/RefusedConfigurationStatusTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.unomi.router.core.route;
+
+import org.apache.camel.component.jackson.JacksonDataFormat;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.router.api.ExportConfiguration;
+import org.apache.unomi.router.api.ImportConfiguration;
+import org.apache.unomi.router.api.ProfileToImport;
+import org.apache.unomi.router.api.RouterConstants;
+import org.apache.unomi.router.api.services.ImportExportConfigurationService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * The permitted base directories are an operational setting, and the 
configurations are user data:
+ * the two drift apart. A configuration that was legitimate when it was 
created can find itself
+ * outside the permitted directories after the deployment is reconfigured, and 
its route then stops
+ * being built.
+ *
+ * <p>Leaving that silent is what makes it painful — the screen keeps showing 
the configuration as
+ * running, and the only trace is a log line nobody reads. So a configuration 
whose endpoint is
+ * refused while its route is being built is recorded as failed, and stays in 
the store: it is for
+ * whoever owns it to correct it or remove it, and they can only do that if 
they can see it.
+ *
+ * <p>Recording it must not schedule the configuration for a route refresh — 
that would have the
+ * refresh rebuild the route, refuse it again, and save it again, indefinitely.
+ */
+public class RefusedConfigurationStatusTest {
+
+    private static final String DEFAULT_ALLOWED_ENDPOINTS = 
"file,ftp,sftp,ftps";
+
+    private static final Map<String, String> NO_KAFKA = new HashMap<>();
+
+    @Rule
+    public TemporaryFolder tmp = new TemporaryFolder();
+
+    private DefaultCamelContext camelContext;
+
+    private File permittedImportDir;
+    private File permittedExportDir;
+    private File arbitraryDir;
+
+    private RecordingConfigurationService<ImportConfiguration> 
importConfigurations;
+    private RecordingConfigurationService<ExportConfiguration> 
exportConfigurations;
+
+    @Before
+    public void setUp() throws Exception {
+        permittedImportDir = tmp.newFolder("permitted-import");
+        permittedExportDir = tmp.newFolder("permitted-export");
+        arbitraryDir = tmp.newFolder("arbitrary");
+        camelContext = new DefaultCamelContext();
+        importConfigurations = new RecordingConfigurationService<>();
+        exportConfigurations = new RecordingConfigurationService<>();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        camelContext.stop();
+    }
+
+    @Test
+    public void aRefusedImportConfigurationIsRecordedAsFailed() throws 
Exception {
+        ImportConfiguration configuration = 
recurrentImport(fileUri(arbitraryDir, "?fileName=profiles.csv"));
+
+        addImportRoutes(configuration);
+
+        assertNull("no route should have been built", 
camelContext.getRouteDefinition("out-of-bounds"));
+        assertEquals("the configuration should be recorded as failed",
+                RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT, 
configuration.getStatus());
+        assertTrue("the configuration should have been saved so the failure is 
visible",
+                importConfigurations.contains("out-of-bounds"));
+    }
+
+    @Test
+    public void aRefusedExportConfigurationIsRecordedAsFailed() throws 
Exception {
+        ExportConfiguration configuration = 
recurrentExport(fileUri(arbitraryDir, "?fileName=profiles.csv"));
+
+        addExportRoutes(configuration);
+
+        assertNull("no route should have been built", 
camelContext.getRouteDefinition("out-of-bounds"));
+        assertEquals("the configuration should be recorded as failed",
+                RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT, 
configuration.getStatus());
+        assertTrue("the configuration should have been saved so the failure is 
visible",
+                exportConfigurations.contains("out-of-bounds"));
+    }
+
+    @Test
+    public void 
recordingARefusedImportConfigurationDoesNotScheduleARouteRefresh() throws 
Exception {
+        addImportRoutes(recurrentImport(fileUri(arbitraryDir, 
"?fileName=profiles.csv")));
+
+        assertFalse("refreshing the route would refuse and save it again, 
without end",
+                importConfigurations.lastSaveAskedForARouteRefresh);
+    }
+
+    @Test
+    public void 
recordingARefusedExportConfigurationDoesNotScheduleARouteRefresh() throws 
Exception {
+        addExportRoutes(recurrentExport(fileUri(arbitraryDir, 
"?fileName=profiles.csv")));
+
+        assertFalse("refreshing the route would refuse and save it again, 
without end",
+                exportConfigurations.lastSaveAskedForARouteRefresh);
+    }
+
+    @Test
+    public void 
anImportConfigurationRecoversWhenItsEndpointBecomesAcceptableAgain() throws 
Exception {
+        ImportConfiguration configuration = 
recurrentImport(fileUri(permittedImportDir, "?fileName=profiles.csv"));
+        
configuration.setStatus(RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT);
+
+        addImportRoutes(configuration);
+
+        assertNull("restoring the permitted directories must bring the 
configuration back on its own",
+                configuration.getStatus());
+        assertTrue("the recovery must be persisted", 
importConfigurations.contains("out-of-bounds"));
+    }
+
+    @Test
+    public void 
anExportConfigurationRecoversWhenItsEndpointBecomesAcceptableAgain() throws 
Exception {
+        ExportConfiguration configuration = 
recurrentExport(fileUri(permittedExportDir, "?fileName=profiles.csv"));
+        
configuration.setStatus(RouterConstants.CONFIG_STATUS_INVALID_ENDPOINT);
+
+        addExportRoutes(configuration);
+
+        assertNull("restoring the permitted directories must bring the 
configuration back on its own",
+                configuration.getStatus());
+        assertTrue("the recovery must be persisted", 
exportConfigurations.contains("out-of-bounds"));
+    }
+
+    @Test
+    public void 
aConfigurationThatFailedItsLastRunKeepsThatStatusWhenItsRouteIsRebuilt() throws 
Exception {
+        ImportConfiguration configuration = 
recurrentImport(fileUri(permittedImportDir, "?fileName=profiles.csv"));
+        configuration.setStatus(RouterConstants.CONFIG_STATUS_COMPLETE_ERRORS);
+
+        addImportRoutes(configuration);
+
+        assertEquals("a failed run is a different matter, and its record must 
survive",
+                RouterConstants.CONFIG_STATUS_COMPLETE_ERRORS, 
configuration.getStatus());
+    }
+
+    @Test
+    public void anAcceptedImportConfigurationIsLeftAlone() throws Exception {
+        ImportConfiguration configuration = 
recurrentImport(fileUri(permittedImportDir, "?fileName=profiles.csv"));
+        configuration.setItemId("in-bounds");
+
+        addImportRoutes(configuration);
+
+        assertNull("an accepted configuration keeps the status it had", 
configuration.getStatus());
+        assertFalse("an accepted configuration is not saved while its route is 
built",
+                importConfigurations.contains("in-bounds"));
+    }
+
+    @Test
+    public void anAcceptedExportConfigurationIsLeftAlone() throws Exception {
+        ExportConfiguration configuration = 
recurrentExport(fileUri(permittedExportDir, "?fileName=profiles.csv"));
+        configuration.setItemId("in-bounds");
+
+        addExportRoutes(configuration);
+
+        assertNull("an accepted configuration keeps the status it had", 
configuration.getStatus());
+        assertFalse("an accepted configuration is not saved while its route is 
built",
+                exportConfigurations.contains("in-bounds"));
+    }
+
+    // 
---------------------------------------------------------------------------------------------
+    // Fixtures
+    // 
---------------------------------------------------------------------------------------------
+
+    private String fileUri(File directory, String suffix) {
+        return "file://" + directory.getAbsolutePath() + suffix;
+    }
+
+    private ImportConfiguration recurrentImport(String source) {
+        ImportConfiguration configuration = new ImportConfiguration();
+        configuration.setItemId("out-of-bounds");
+        
configuration.setConfigType(RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT);
+        configuration.setActive(true);
+        configuration.getProperties().put("source", source);
+        configuration.getProperties().put("mapping", 
Collections.singletonMap("0", 0));
+        return configuration;
+    }
+
+    private ExportConfiguration recurrentExport(String destination) {
+        ExportConfiguration configuration = new ExportConfiguration();
+        configuration.setItemId("out-of-bounds");
+        
configuration.setConfigType(RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT);
+        configuration.setActive(true);
+        configuration.getProperties().put("destination", destination);
+        configuration.getProperties().put("mapping", 
Collections.singletonMap("0", "firstName"));
+        configuration.getProperties().put("segment", "exportSegment");
+        configuration.getProperties().put("period", "1m");
+        return configuration;
+    }
+
+    private void addImportRoutes(ImportConfiguration... configurations) throws 
Exception {
+        ProfileImportFromSourceRouteBuilder builder =
+                new ProfileImportFromSourceRouteBuilder(NO_KAFKA, 
RouterConstants.CONFIG_TYPE_NOBROKER);
+        
builder.setImportConfigurationList(java.util.Arrays.asList(configurations));
+        builder.setImportConfigurationService(importConfigurations);
+        builder.setProfileService(noOpProfileService());
+        builder.setJacksonDataFormat(new 
JacksonDataFormat(ProfileToImport.class));
+        builder.setAllowedEndpoints(DEFAULT_ALLOWED_ENDPOINTS);
+        
builder.setPermittedImportBaseDirs(permittedImportDir.getAbsolutePath());
+        builder.setContext(camelContext);
+        camelContext.addRoutes(builder);
+    }
+
+    private void addExportRoutes(ExportConfiguration... configurations) throws 
Exception {
+        ProfileExportCollectRouteBuilder builder =
+                new ProfileExportCollectRouteBuilder(NO_KAFKA, 
RouterConstants.CONFIG_TYPE_NOBROKER);
+        
builder.setExportConfigurationList(java.util.Arrays.asList(configurations));
+        builder.setExportConfigurationService(exportConfigurations);
+        builder.setJacksonDataFormat(new 
JacksonDataFormat(ProfileToImport.class));
+        builder.setAllowedEndpoints(DEFAULT_ALLOWED_ENDPOINTS);
+        
builder.setPermittedExportBaseDirs(permittedExportDir.getAbsolutePath());
+        builder.setContext(camelContext);
+        camelContext.addRoutes(builder);
+    }
+
+    private static ProfileService noOpProfileService() {
+        return (ProfileService) Proxy.newProxyInstance(
+                ProfileService.class.getClassLoader(),
+                new Class<?>[]{ProfileService.class},
+                (proxy, method, args) -> 
java.util.Collection.class.isAssignableFrom(method.getReturnType())
+                        ? Collections.emptyList() : null);
+    }
+
+    /**
+     * Stores what it is given, and remembers whether the last save asked for 
the running route to be
+     * refreshed — a refused configuration must not, or the refresh loops.
+     */
+    private static final class RecordingConfigurationService<T> implements 
ImportExportConfigurationService<T> {
+
+        private final Map<String, T> stored = new LinkedHashMap<>();
+
+        private boolean lastSaveAskedForARouteRefresh;
+
+        boolean contains(String configId) {
+            return stored.containsKey(configId);
+        }
+
+        @Override
+        public List<T> getAll() {
+            return new ArrayList<>(stored.values());
+        }
+
+        @Override
+        public T load(String configId) {
+            return stored.get(configId);
+        }
+
+        @Override
+        public T save(T configuration, boolean updateRunningRoute) {
+            lastSaveAskedForARouteRefresh = updateRunningRoute;
+            stored.put(itemIdOf(configuration), configuration);
+            return configuration;
+        }
+
+        @Override
+        public void delete(String configId) {
+            stored.remove(configId);
+        }
+
+        @Override
+        public Map<String, RouterConstants.CONFIG_CAMEL_REFRESH> 
consumeConfigsToBeRefresh() {
+            return Collections.emptyMap();
+        }
+
+        private String itemIdOf(T configuration) {
+            return configuration instanceof ImportConfiguration
+                    ? ((ImportConfiguration) configuration).getItemId()
+                    : ((ExportConfiguration) configuration).getItemId();
+        }
+    }
+}

Reply via email to