From bba58415609b99128dde5e528bafd950fcaf97bf Mon Sep 17 00:00:00 2001 From: mattcasters Date: Thu, 22 Oct 2026 17:31:05 +0200 Subject: [PATCH 1/2] Issue #9089 : Add local run configurations to new Hop setups Setup copied only the top level of the default project template, so a new configuration was created without the local pipeline and workflow run configurations. Copy the full template and add those configurations when they are missing. --- .../ROOT/pages/hop-tools/hop-setup.adoc | 2 +- .../apache/hop/setup/persist/HopVfsFiles.java | 3 +- .../persist/LocalRunConfigurationSeeder.java | 230 ++++++++++++++++++ .../persist/ProjectRegistrationWriter.java | 18 +- .../gui/messages/messages_en_US.properties | 2 +- .../setup/persist/ConfigFolderSeederTest.java | 10 + .../LocalRunConfigurationSeederTest.java | 107 ++++++++ .../ProjectRegistrationWriterTest.java | 52 +++- 8 files changed, 418 insertions(+), 6 deletions(-) create mode 100644 plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/LocalRunConfigurationSeeder.java create mode 100644 plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/LocalRunConfigurationSeederTest.java diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-setup.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-setup.adoc index d8a9ebb7b99..85fbf9b3944 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-setup.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-setup.adoc @@ -86,7 +86,7 @@ image::hop-tools/setup/06-configure-hop-environment-dialog-projects-tab.png[Conf ** Windows: `%USERPROFILE%\Documents\Hop\default` * Register the *samples* project from this Hop installation (`/config/projects/samples`) so the examples stay available after the config folder moves. -Both options are selected by default. Apply writes them into the new `hop-config.json` (and creates `project-config.json` plus the install default-project template when the default home is empty). +Both options are selected by default. Apply writes them into the new `hop-config.json`. An empty default project home is created from the install default-project template. That project includes a `local` pipeline run configuration and a `local` workflow run configuration. If the template does not contain them, setup adds them, together with the local execution info location and data profile they use. === Preview and apply diff --git a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java index 1eb951dcdca..1cb52fa186b 100644 --- a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java +++ b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java @@ -79,7 +79,8 @@ public static void copyTree(String sourcePath, String destinationPath) throws Ho if (!destination.exists()) { destination.createFolder(); } - destination.copyFrom(source, Selectors.SELECT_CHILDREN); + // SELECT_CHILDREN copies only the first level, which leaves metadata folders empty. + destination.copyFrom(source, Selectors.EXCLUDE_SELF); } catch (Exception e) { throw new HopSetupException( "Unable to copy '" + sourcePath + "' to '" + destinationPath + "'", e); diff --git a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/LocalRunConfigurationSeeder.java b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/LocalRunConfigurationSeeder.java new file mode 100644 index 00000000000..e8c0ec1d768 --- /dev/null +++ b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/LocalRunConfigurationSeeder.java @@ -0,0 +1,230 @@ +/* + * 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.hop.setup.persist; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.setup.HopEnvironmentApplyResult; +import org.apache.hop.setup.HopSetupException; + +/** + * Adds the standard {@code local} pipeline and workflow run configurations to a new project. + * + *

Files already present are left unchanged. A file that exists in the install default-project + * template is copied from there. Otherwise the stock default-project content is written, including + * the local execution info location and data profile those run configurations use. + */ +public class LocalRunConfigurationSeeder { + + static final String PIPELINE_RELATIVE = "metadata/pipeline-run-configuration/local.json"; + static final String WORKFLOW_RELATIVE = "metadata/workflow-run-configuration/local.json"; + static final String EXECUTION_INFO_RELATIVE = "metadata/execution-info-location/local-audit.json"; + static final String DATA_PROFILE_RELATIVE = "metadata/execution-data-profile/first-last.json"; + + static final String PIPELINE_JSON = + """ + { + "engineRunConfiguration": { + "Local": { + "feedback_size": "50000", + "sample_size": "100", + "sample_type_in_gui": "Last", + "rowset_size": "10000", + "safe_mode": false, + "show_feedback": false, + "topo_sort": false, + "gather_metrics": false + } + }, + "name": "local", + "configurationVariables": [], + "description": "", + "executionInfoLocationName": "local-audit", + "dataProfile": "first-last", + "defaultSelection": true + } + """; + + static final String WORKFLOW_JSON = + """ + { + "engineRunConfiguration": { + "Local": { + "safe_mode": false + } + }, + "name": "local", + "description": "", + "executionInfoLocationName": "local-audit", + "defaultSelection": true + } + """; + + static final String EXECUTION_INFO_JSON = + """ + { + "executionInfoLocation": { + "local-folder": { + "pluginName": "File location", + "pluginId": "local-folder", + "rootFolder": "${HOP_AUDIT_FOLDER}/executions/" + } + }, + "dataLoggingDelay": "2000", + "name": "local-audit", + "description": "", + "dataLoggingInterval": "5000" + } + """; + + static final String DATA_PROFILE_JSON = + """ + { + "name": "first-last", + "description": "capture the first and last 100 rows of every Hop transform", + "sampler": [ + { + "FirstRowsExecutionDataSampler": { + "sampleSize": "100" + } + }, + { + "LastRowsExecutionDataSampler": { + "sampleSize": "100" + } + } + ] + } + """; + + public boolean standardRunConfigurationsMissing(String templateHome) throws HopSetupException { + return !templateContains(templateHome, PIPELINE_RELATIVE) + || !templateContains(templateHome, WORKFLOW_RELATIVE); + } + + public void ensure(String projectHome, String templateHome, HopEnvironmentApplyResult result) + throws HopSetupException { + boolean synthesized = + ensureRunConfiguration( + projectHome, + templateHome, + PIPELINE_RELATIVE, + PIPELINE_JSON, + "local pipeline run configuration", + result); + synthesized = + ensureRunConfiguration( + projectHome, + templateHome, + WORKFLOW_RELATIVE, + WORKFLOW_JSON, + "local workflow run configuration", + result) + || synthesized; + if (synthesized) { + ensureFile( + projectHome, + templateHome, + EXECUTION_INFO_RELATIVE, + EXECUTION_INFO_JSON, + "local execution info location", + result); + ensureFile( + projectHome, + templateHome, + DATA_PROFILE_RELATIVE, + DATA_PROFILE_JSON, + "execution data profile", + result); + } else { + copyFromTemplateIfMissing( + projectHome, + templateHome, + EXECUTION_INFO_RELATIVE, + "local execution info location", + result); + copyFromTemplateIfMissing( + projectHome, templateHome, DATA_PROFILE_RELATIVE, "execution data profile", result); + } + } + + private boolean ensureRunConfiguration( + String projectHome, + String templateHome, + String relative, + String fallback, + String label, + HopEnvironmentApplyResult result) + throws HopSetupException { + String destination = projectHome + "/" + relative; + if (HopVfsFiles.exists(destination)) { + return false; + } + if (templateContains(templateHome, relative)) { + write(destination, HopVfsFiles.readUtf8(templateHome + "/" + relative), label, result); + return false; + } + write(destination, fallback, label, result); + return true; + } + + private void ensureFile( + String projectHome, + String templateHome, + String relative, + String fallback, + String label, + HopEnvironmentApplyResult result) + throws HopSetupException { + String destination = projectHome + "/" + relative; + if (HopVfsFiles.exists(destination)) { + return; + } + String content = + templateContains(templateHome, relative) + ? HopVfsFiles.readUtf8(templateHome + "/" + relative) + : fallback; + write(destination, content, label, result); + } + + private void copyFromTemplateIfMissing( + String projectHome, + String templateHome, + String relative, + String label, + HopEnvironmentApplyResult result) + throws HopSetupException { + String destination = projectHome + "/" + relative; + if (HopVfsFiles.exists(destination) || !templateContains(templateHome, relative)) { + return; + } + write(destination, HopVfsFiles.readUtf8(templateHome + "/" + relative), label, result); + } + + private static boolean templateContains(String templateHome, String relative) + throws HopSetupException { + return StringUtils.isNotBlank(templateHome) + && HopVfsFiles.exists(templateHome + "/" + relative); + } + + private static void write( + String destination, String content, String label, HopEnvironmentApplyResult result) + throws HopSetupException { + HopVfsFiles.writeUtf8(destination, content); + result.addMessage("Created " + label + " at " + destination); + } +} diff --git a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ProjectRegistrationWriter.java b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ProjectRegistrationWriter.java index edb7d6bf6bf..bfedb21a38d 100644 --- a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ProjectRegistrationWriter.java +++ b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ProjectRegistrationWriter.java @@ -35,8 +35,10 @@ /** * Creates a user-owned default project and registers it (plus the install samples project) in the - * target {@code hop-config.json}. Does not use the live {@code HopConfig} singleton, which still - * points at the current process config folder. + * target {@code hop-config.json}. A new default project gets a {@code local} pipeline run + * configuration and a {@code local} workflow run configuration when it does not already have them. + * Does not use the live {@code HopConfig} singleton, which still points at the current process + * config folder. */ public class ProjectRegistrationWriter { @@ -55,10 +57,17 @@ public class ProjectRegistrationWriter { """; private final ObjectMapper mapper; + private final LocalRunConfigurationSeeder runConfigurations; public ProjectRegistrationWriter() { + this(new LocalRunConfigurationSeeder()); + } + + ProjectRegistrationWriter(LocalRunConfigurationSeeder runConfigurations) { mapper = HopJson.newMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); + this.runConfigurations = + runConfigurations == null ? new LocalRunConfigurationSeeder() : runConfigurations; } public void apply( @@ -118,6 +127,10 @@ private void seedDefaultProject( hasTemplate ? "Would copy default project template to " + defaultHome : "Would create default project at " + defaultHome); + if (runConfigurations.standardRunConfigurationsMissing(hasTemplate ? template : null)) { + result.addMessage( + "Would add local pipeline and workflow run configurations to " + defaultHome); + } return; } HopVfsFiles.createFolder(defaultHome); @@ -128,6 +141,7 @@ private void seedDefaultProject( HopVfsFiles.writeUtf8(projectConfig, MINIMAL_PROJECT_CONFIG); result.addMessage("Created default project at " + defaultHome); } + runConfigurations.ensure(defaultHome, hasTemplate ? template : null, result); } private String resolveSamplesHome(Path hopHome, HopEnvironmentApplyResult result) diff --git a/plugins/misc/setup/src/main/resources/org/apache/hop/setup/gui/messages/messages_en_US.properties b/plugins/misc/setup/src/main/resources/org/apache/hop/setup/gui/messages/messages_en_US.properties index 1d5bda47f25..cd80d9ce51b 100644 --- a/plugins/misc/setup/src/main/resources/org/apache/hop/setup/gui/messages/messages_en_US.properties +++ b/plugins/misc/setup/src/main/resources/org/apache/hop/setup/gui/messages/messages_en_US.properties @@ -26,7 +26,7 @@ SetupDialog.Web.ReadOnly=Hop Web cannot write the server operating system enviro SetupDialog.Tab.Variables=Variables SetupDialog.Tab.Location=Location SetupDialog.Tab.Projects=Projects -SetupDialog.Projects.Intro=After you point Hop at a new config folder it starts without projects. Create a default project in your documents folder and register the samples project from this Hop installation. +SetupDialog.Projects.Intro=After you point Hop at a new config folder it starts without projects. Create a default project in your documents folder, including a local pipeline and workflow run configuration, and register the samples project from this Hop installation. SetupDialog.Projects.CreateDefault=Create a default project SetupDialog.Projects.DefaultHome.Label=Default project home SetupDialog.Projects.Recommended=Use recommended project folder diff --git a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java index 53a92bf3742..f51ca9af3d6 100644 --- a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java +++ b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java @@ -52,6 +52,12 @@ void copiesInstallConfigWhenTargetEmpty() throws Exception { Files.createDirectories(install.resolve("config")); Files.writeString(install.resolve("config").resolve(HopSetupVariables.HOP_CONFIG_JSON), "{}"); Files.writeString(install.resolve("config").resolve("extra.txt"), "keep"); + Path nested = + install + .resolve("config/projects/default/metadata/pipeline-run-configuration") + .resolve("local.json"); + Files.createDirectories(nested.getParent()); + Files.writeString(nested, "{ \"name\": \"local\" }"); Path target = temp.resolve("user-config"); HopEnvironmentSpec spec = new HopEnvironmentSpec(); spec.setConfigFolder(target.toString()); @@ -60,6 +66,10 @@ void copiesInstallConfigWhenTargetEmpty() throws Exception { new ConfigFolderSeeder().seed(spec, new HopEnvironmentApplyResult(), install); assertTrue(Files.exists(target.resolve(HopSetupVariables.HOP_CONFIG_JSON))); assertTrue(Files.exists(target.resolve("extra.txt"))); + assertTrue( + Files.readString( + target.resolve("projects/default/metadata/pipeline-run-configuration/local.json")) + .contains("\"name\": \"local\"")); } @Test diff --git a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/LocalRunConfigurationSeederTest.java b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/LocalRunConfigurationSeederTest.java new file mode 100644 index 00000000000..a6e731dece4 --- /dev/null +++ b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/LocalRunConfigurationSeederTest.java @@ -0,0 +1,107 @@ +/* + * 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.hop.setup.persist; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.hop.setup.HopEnvironmentApplyResult; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class LocalRunConfigurationSeederTest { + + @TempDir Path temp; + + @Test + void writesStandardLocalRunConfigurationsWhenTemplateHasNone() throws Exception { + Path project = temp.resolve("default"); + Files.createDirectories(project); + HopEnvironmentApplyResult result = new HopEnvironmentApplyResult(); + + new LocalRunConfigurationSeeder().ensure(project.toString(), null, result); + + String pipeline = + Files.readString(project.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE)); + String workflow = + Files.readString(project.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE)); + assertTrue(pipeline.contains("\"name\": \"local\"")); + assertTrue(pipeline.contains("\"defaultSelection\": true")); + assertTrue(pipeline.contains("\"executionInfoLocationName\": \"local-audit\"")); + assertTrue(pipeline.contains("\"dataProfile\": \"first-last\"")); + assertTrue(workflow.contains("\"name\": \"local\"")); + assertTrue(workflow.contains("\"defaultSelection\": true")); + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.EXECUTION_INFO_RELATIVE)) + .contains("${HOP_AUDIT_FOLDER}/executions/")); + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.DATA_PROFILE_RELATIVE)) + .contains("first-last")); + assertTrue(result.describe().contains("local pipeline run configuration")); + assertTrue(result.describe().contains("local workflow run configuration")); + } + + @Test + void keepsTemplateRunConfigurationsAndDoesNotInventCompanions() throws Exception { + Path template = temp.resolve("template"); + write( + template, LocalRunConfigurationSeeder.PIPELINE_RELATIVE, "{ \"name\": \"kept-pipeline\" }"); + write( + template, LocalRunConfigurationSeeder.WORKFLOW_RELATIVE, "{ \"name\": \"kept-workflow\" }"); + Path project = temp.resolve("default"); + Files.createDirectories(project); + + new LocalRunConfigurationSeeder() + .ensure(project.toString(), template.toString(), new HopEnvironmentApplyResult()); + + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE)) + .contains("kept-pipeline")); + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE)) + .contains("kept-workflow")); + assertFalse(Files.exists(project.resolve(LocalRunConfigurationSeeder.EXECUTION_INFO_RELATIVE))); + assertFalse(Files.exists(project.resolve(LocalRunConfigurationSeeder.DATA_PROFILE_RELATIVE))); + } + + @Test + void doesNotReplaceExistingRunConfigurations() throws Exception { + Path project = temp.resolve("default"); + write(project, LocalRunConfigurationSeeder.PIPELINE_RELATIVE, "{ \"name\": \"mine\" }"); + write(project, LocalRunConfigurationSeeder.WORKFLOW_RELATIVE, "{ \"name\": \"mine-wf\" }"); + + new LocalRunConfigurationSeeder() + .ensure(project.toString(), null, new HopEnvironmentApplyResult()); + + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE)) + .contains("mine")); + assertTrue( + Files.readString(project.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE)) + .contains("mine-wf")); + assertFalse(Files.exists(project.resolve(LocalRunConfigurationSeeder.EXECUTION_INFO_RELATIVE))); + } + + private static void write(Path root, String relative, String content) throws Exception { + Path file = root.resolve(relative); + Files.createDirectories(file.getParent()); + Files.writeString(file, content); + } +} diff --git a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java index d80345b54b7..c2e79ae13da 100644 --- a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java +++ b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java @@ -79,6 +79,12 @@ void createsDefaultProjectAndRegistersSamples() throws Exception { hopHome); assertTrue(Files.exists(defaultHome.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME))); + assertTrue( + Files.readString(defaultHome.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE)) + .contains("\"name\": \"local\"")); + assertTrue( + Files.readString(defaultHome.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE)) + .contains("\"name\": \"local\"")); String hopConfig = Files.readString(configFolder.resolve(HopSetupVariables.HOP_CONFIG_JSON)); assertTrue(hopConfig.contains("\"projectName\" : \"default\"")); assertTrue(hopConfig.contains(defaultHome.toString())); @@ -87,6 +93,46 @@ void createsDefaultProjectAndRegistersSamples() throws Exception { assertTrue(hopConfig.contains("\"defaultProject\" : \"default\"")); } + @Test + void copiesNestedTemplateRunConfigurations() throws Exception { + Path hopHome = temp.resolve("hop"); + Path template = hopHome.resolve("config/projects/default"); + Files.createDirectories(template.resolve("metadata/pipeline-run-configuration")); + Files.createDirectories(template.resolve("metadata/workflow-run-configuration")); + Files.writeString( + template.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME), + "{ \"description\": \"template\" }"); + Files.writeString( + template.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE), + "{ \"name\": \"from-template\" }"); + Files.writeString( + template.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE), + "{ \"name\": \"from-template-wf\" }"); + + Path defaultHome = temp.resolve("Documents/hop/default"); + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setConfigFolder(temp.resolve("user-config").toString()); + spec.setCreateDefaultProject(true); + spec.setDefaultProjectHome(defaultHome.toString()); + + new ProjectRegistrationWriter() + .apply( + spec, + OsFamily.UNIX, + unixPaths(temp.resolve("home")), + new HopEnvironmentApplyResult(), + hopHome); + + assertTrue( + Files.readString(defaultHome.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE)) + .contains("from-template")); + assertTrue( + Files.readString(defaultHome.resolve(LocalRunConfigurationSeeder.WORKFLOW_RELATIVE)) + .contains("from-template-wf")); + assertFalse( + Files.exists(defaultHome.resolve(LocalRunConfigurationSeeder.EXECUTION_INFO_RELATIVE))); + } + @Test void doesNotOverwriteExistingDefaultProjectConfig() throws Exception { Path defaultHome = temp.resolve("docs/hop/default"); @@ -112,6 +158,7 @@ void doesNotOverwriteExistingDefaultProjectConfig() throws Exception { assertTrue( Files.readString(defaultHome.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME)) .contains("keep")); + assertFalse(Files.exists(defaultHome.resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE))); } @Test @@ -130,7 +177,10 @@ void dryRunDoesNotWriteHopConfig() throws Exception { .apply(spec, OsFamily.UNIX, unixPaths(temp.resolve("home")), result, null); assertFalse(Files.exists(configFolder.resolve(HopSetupVariables.HOP_CONFIG_JSON))); - assertTrue(result.describe().contains("Would")); + assertTrue( + result.describe().contains("Would add local pipeline and workflow run configurations")); + assertFalse( + Files.exists(temp.resolve("p").resolve(LocalRunConfigurationSeeder.PIPELINE_RELATIVE))); } @Test From ac04367e07f4797a456448ca35dc777ccce11858 Mon Sep 17 00:00:00 2001 From: Matt Casters Date: Fri, 23 Oct 2026 06:09:34 +0000 Subject: [PATCH 2/2] Issue #9089 : Copy nested folders only for the default project template --- .../hop/setup/persist/ConfigFolderSeeder.java | 3 +- .../apache/hop/setup/persist/HopVfsFiles.java | 19 +++++++++-- .../setup/persist/ConfigFolderSeederTest.java | 18 +++++++---- .../ProjectRegistrationWriterTest.java | 32 +++++++++++++++++++ 4 files changed, 62 insertions(+), 10 deletions(-) diff --git a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ConfigFolderSeeder.java b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ConfigFolderSeeder.java index 15124e9e4c9..fde50e0266f 100644 --- a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ConfigFolderSeeder.java +++ b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/ConfigFolderSeeder.java @@ -67,7 +67,8 @@ public void seed(HopEnvironmentSpec spec, HopEnvironmentApplyResult result, Path result.addMessage("Would copy " + source + " to " + spec.getConfigFolder()); return; } - HopVfsFiles.copyTree(source, spec.getConfigFolder()); + // One level only: nested projects/default and projects/samples stay in the install tree. + HopVfsFiles.copyChildren(source, spec.getConfigFolder()); result.addMessage("Copied install config from " + source + " to " + spec.getConfigFolder()); } diff --git a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java index 1cb52fa186b..123a0dfb44e 100644 --- a/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java +++ b/plugins/misc/setup/src/main/java/org/apache/hop/setup/persist/HopVfsFiles.java @@ -21,6 +21,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSelector; import org.apache.commons.vfs2.Selectors; import org.apache.hop.core.vfs.HopVfs; import org.apache.hop.setup.HopSetupException; @@ -72,15 +73,29 @@ public static void createFolder(String path) throws HopSetupException { } } + /** + * Copies the direct children of {@code sourcePath} into {@code destinationPath}. Child folders + * are created without their contents. + */ + public static void copyChildren(String sourcePath, String destinationPath) + throws HopSetupException { + copySelected(sourcePath, destinationPath, Selectors.SELECT_CHILDREN); + } + + /** Copies every file and subfolder under {@code sourcePath} into {@code destinationPath}. */ public static void copyTree(String sourcePath, String destinationPath) throws HopSetupException { + copySelected(sourcePath, destinationPath, Selectors.EXCLUDE_SELF); + } + + private static void copySelected(String sourcePath, String destinationPath, FileSelector selector) + throws HopSetupException { try { FileObject source = HopVfs.getFileObject(sourcePath); FileObject destination = HopVfs.getFileObject(destinationPath); if (!destination.exists()) { destination.createFolder(); } - // SELECT_CHILDREN copies only the first level, which leaves metadata folders empty. - destination.copyFrom(source, Selectors.EXCLUDE_SELF); + destination.copyFrom(source, selector); } catch (Exception e) { throw new HopSetupException( "Unable to copy '" + sourcePath + "' to '" + destinationPath + "'", e); diff --git a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java index f51ca9af3d6..496cb2b1478 100644 --- a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java +++ b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ConfigFolderSeederTest.java @@ -52,12 +52,17 @@ void copiesInstallConfigWhenTargetEmpty() throws Exception { Files.createDirectories(install.resolve("config")); Files.writeString(install.resolve("config").resolve(HopSetupVariables.HOP_CONFIG_JSON), "{}"); Files.writeString(install.resolve("config").resolve("extra.txt"), "keep"); - Path nested = + Path defaultProject = install .resolve("config/projects/default/metadata/pipeline-run-configuration") .resolve("local.json"); - Files.createDirectories(nested.getParent()); - Files.writeString(nested, "{ \"name\": \"local\" }"); + Files.createDirectories(defaultProject.getParent()); + Files.writeString(defaultProject, "{ \"name\": \"local\" }"); + Path samplesProject = install.resolve("config/projects/samples"); + Files.createDirectories(samplesProject); + Files.writeString( + samplesProject.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME), + "{ \"description\": \"samples\" }"); Path target = temp.resolve("user-config"); HopEnvironmentSpec spec = new HopEnvironmentSpec(); spec.setConfigFolder(target.toString()); @@ -66,10 +71,9 @@ void copiesInstallConfigWhenTargetEmpty() throws Exception { new ConfigFolderSeeder().seed(spec, new HopEnvironmentApplyResult(), install); assertTrue(Files.exists(target.resolve(HopSetupVariables.HOP_CONFIG_JSON))); assertTrue(Files.exists(target.resolve("extra.txt"))); - assertTrue( - Files.readString( - target.resolve("projects/default/metadata/pipeline-run-configuration/local.json")) - .contains("\"name\": \"local\"")); + assertTrue(Files.isDirectory(target.resolve("projects"))); + assertFalse(Files.exists(target.resolve("projects/default"))); + assertFalse(Files.exists(target.resolve("projects/samples"))); } @Test diff --git a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java index c2e79ae13da..b4405029453 100644 --- a/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java +++ b/plugins/misc/setup/src/test/java/org/apache/hop/setup/persist/ProjectRegistrationWriterTest.java @@ -133,6 +133,38 @@ void copiesNestedTemplateRunConfigurations() throws Exception { Files.exists(defaultHome.resolve(LocalRunConfigurationSeeder.EXECUTION_INFO_RELATIVE))); } + @Test + void copiesNestedTemplateSubfolders() throws Exception { + Path hopHome = temp.resolve("hop"); + Path template = hopHome.resolve("config/projects/default"); + Files.createDirectories(template.resolve("metadata/rdbms")); + Files.createDirectories(template.resolve("datasets")); + Files.writeString( + template.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME), + "{ \"description\": \"template\" }"); + Files.writeString(template.resolve("metadata/rdbms/sample.json"), "{ \"name\": \"sample\" }"); + Files.writeString(template.resolve("datasets/customers.csv"), "id,name"); + + Path defaultHome = temp.resolve("Documents/hop/default"); + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setConfigFolder(temp.resolve("user-config").toString()); + spec.setCreateDefaultProject(true); + spec.setDefaultProjectHome(defaultHome.toString()); + + new ProjectRegistrationWriter() + .apply( + spec, + OsFamily.UNIX, + unixPaths(temp.resolve("home")), + new HopEnvironmentApplyResult(), + hopHome); + + assertTrue( + Files.readString(defaultHome.resolve("metadata/rdbms/sample.json")).contains("sample")); + assertTrue(Files.readString(defaultHome.resolve("datasets/customers.csv")).contains("id,name")); + assertTrue(Files.exists(defaultHome.resolve(HopSetupVariables.PROJECT_CONFIG_FILENAME))); + } + @Test void doesNotOverwriteExistingDefaultProjectConfig() throws Exception { Path defaultHome = temp.resolve("docs/hop/default");