This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 369c499bb7 Issue #4881 : Add environment configuration files from a
folder and wildcard (#8694)
369c499bb7 is described below
commit 369c499bb7eb9bd537a854d48659d025d356fbf5
Author: Matt Casters <[email protected]>
AuthorDate: Thu Oct 1 12:03:05 2026 +0200
Issue #4881 : Add environment configuration files from a folder and
wildcard (#8694)
---
.../hop-tools/hop-conf/hop-conf-projects.adoc | 25 ++
.../ROOT/pages/hop-tools/hop-conf/hop-conf.adoc | 20 ++
.../ROOT/pages/projects/projects-environments.adoc | 11 +-
.../environment/EnvironmentConfigFileSelector.java | 182 +++++++++++++++
.../environment/LifecycleEnvironmentDialog.java | 259 ++++++++++++++++++++-
.../ManageEnvironmentsOptionPlugin.java | 66 +++++-
.../environment/messages/messages_en_US.properties | 19 ++
.../EnvironmentConfigFileSelectorTest.java | 185 +++++++++++++++
.../ManageEnvironmentsOptionPluginTest.java | 125 ++++++++++
9 files changed, 876 insertions(+), 16 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
index 363dec2f51..78f082e1d6 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf-projects.adoc
@@ -99,6 +99,31 @@ As you can see from the log, an empty file was created to
set variables in:
{ }
----
+==== Adding configuration files from a directory
+
+`--environment-config-files` still lists files one by one.
+To add every matching file in a folder, pass the folder and a regular
expression.
+The expression is matched against the file name, the same way as in the JSON
Input and Get File Names transforms.
+`*.json` is not a valid expression; use `.*\.json`.
+
+An exclusion expression and `--environment-config-include-subfolders` are
optional.
+Files found in the folder are stored as individual paths.
+They can be combined with `--environment-config-files`.
+When either option is set, that selection replaces the environment's
configuration file list.
+
+[source,bash]
+----
+$ sh hop-conf.sh \
+ --environment-create \
+ --environment test-file \
+ --environment-project test-file \
+ --environment-purpose=Testing \
+ --environment-config-file-directory="<YOUR_ENV_CONFIG_PATH>/config" \
+ --environment-config-file-wildcard=".*\.json" \
+ --environment-config-file-exclude-wildcard=".*secret.*" \
+ --environment-config-include-subfolders
+----
+
==== Setting variables in an environment
This command adds a variable to the environment configuration file:
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
index 03264ed6b2..16c4df2272 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc
@@ -93,6 +93,22 @@ Usage: <main class> [-h] [-ec] [-ed] [-el] [-em] [-ey] [-pc]
[-pd] [-pl] [-pm]
<environmentConfigFiles>...]
A list of configuration files for this lifecycle
environment, comma separated
+ --environment-config-file-directory=<environmentConfigFileDirectory>
+ Folder of configuration files for this lifecycle
+ environment. Matching files replace the
configuration
+ file list, together with
--environment-config-files
+ when that option is also set.
+
--environment-config-file-exclude-wildcard=<environmentConfigFileExcludeWildcard>
+ Regular expression for file names to ignore in
+ --environment-config-file-directory. Example:
+ .*secret.*
+ --environment-config-file-wildcard=<environmentConfigFileWildcard>
+ Regular expression matched against file names in
+ --environment-config-file-directory. Example:
+ .*\.json
+ --environment-config-include-subfolders
+ Also include matching files from subfolders of
+ --environment-config-file-directory
-el, --environments-list
List the defined lifecycle environments
-em, --environment-modify
@@ -223,6 +239,10 @@ TIP: `-sv` can be used to unset a variable by specifying a
variable without a va
|-cfd|--config-file-describe-variables=<configDescribeVariables>[,<configDescribeVariables>...]|A
list of variable=description combinations separated by a comma
|-cfv|--config-file-set-variables=<configSetVariables>
,<configSetVariables>...]|A list of variable=value combinations separated by a
comma
|-eg|--environment-config-files=<environmentConfigFiles>[,
<environmentConfigFiles>...]|A list of configuration files for this lifecycle
environment, comma separated
+||--environment-config-file-directory=<environmentConfigFileDirectory>|Folder
of configuration files. Matching files replace the configuration file list,
together with --environment-config-files when that option is also set. The
wildcard is a regular expression, for example `.*\.json`, not a shell glob
+||--environment-config-file-wildcard=<environmentConfigFileWildcard>|Regular
expression matched against file names in --environment-config-file-directory
+||--environment-config-file-exclude-wildcard=<environmentConfigFileExcludeWildcard>|Regular
expression for file names to ignore in that folder
+||--environment-config-include-subfolders|Also include matching files from
subfolders of --environment-config-file-directory
|-pv|--project-variables=<projectVariables>[,<projectVariables>...]| A list of
variable=value combinations separated by a comma
|===
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
index 0894a872e5..7d5b61fbcc 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
@@ -147,14 +147,23 @@
image:hop-gui/environment/environment-dialog-configuration-files-tab.png[Environ
|Property|Description|Variables Supported|Mandatory|Default
|Configuration files|One or more files that define environment-specific
variables. Each row is a filename and a description|Yes|No|
|Description|Short note about that file. It is stored in the JSON file, not on
the environment|No|No|
+|File or directory|A single configuration file, or a folder to scan. *Add*
puts the matching files into the list below|Yes|No|
+|Inclusion wildcard (RegExp)|Regular expression matched against the file name,
for example `.*\.json`. Leave empty to include every file. Not a shell
glob|Yes|No|
+|Exclusion wildcard (RegExp)|Regular expression for file names to skip, for
example `.*secret.*`|Yes|No|
+|Include subfolders|Also add matching files from subfolders|No|No|
+|Add|Resolve the file or folder and append each match to the list. The
environment still stores one path per file|No|No|
|Import variables|Scan the project for `+${VARIABLE}+` expressions and add
missing names to a configuration file|No|No|
-|Select...|Add an existing configuration file|No|No|
+|Select...|Add one or more existing configuration files. Several files can be
selected at once|No|No|
|New...|Create a new configuration file and add it to the environment|No|No|
|Edit...|Open the selected configuration file to set its description and the
variable names, values and descriptions|No|No|
|===
Each environment contains one or more configuration files where you manage
environment-specific variables.
+*Add* expands a file or a folder into that list immediately, using the same
regular-expression rules as the JSON Input and Get File Names transforms.
+A folder is not stored as a wildcard.
+After the files are added, each row is still one file, so *Edit...* opens that
file.
+
Store secrets (database passwords, API keys, tokens) in these files as
variables, not in project metadata or pipelines.
Keep the files *outside* the project folder so they are not checked into the
project repository.
Better still, store the value in a keystore or vault and put a
xref:metadata-types/variable-resolver/index.adoc[variable resolver] expression
in the configuration file.
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
new file mode 100644
index 0000000000..b75fef7abe
--- /dev/null
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelector.java
@@ -0,0 +1,182 @@
+/*
+ * 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.projects.environment;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.fileinput.FileInputList;
+import org.apache.hop.core.fileinput.FileTypeFilter;
+import org.apache.hop.core.fileinput.InputFile;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.vfs.HopVfs;
+
+/**
+ * Resolves environment configuration files from one file or from a directory.
+ *
+ * <p>Wildcards are regular expressions matched against the file name, using
the same rules as
+ * {@link FileInputList}. They are not shell globs: {@code .*\.json} matches
JSON files and {@code
+ * *.json} is rejected.
+ */
+public final class EnvironmentConfigFileSelector {
+
+ private EnvironmentConfigFileSelector() {}
+
+ /**
+ * Files selected by an explicit list and, when a directory is set, by a
directory scan.
+ *
+ * <p>Explicit paths stay in the given order. Directory matches are appended
and a file already
+ * present is not added again. A wildcard, an exclude wildcard, or
subfolders without a directory
+ * is an error. An empty directory argument with no extra options returns
the explicit list, which
+ * may be empty.
+ */
+ public static List<String> combine(
+ IVariables variables,
+ String[] explicitFiles,
+ String directory,
+ String wildcard,
+ String excludeWildcard,
+ boolean includeSubFolders)
+ throws HopException {
+ boolean directorySet =
StringUtils.isNotEmpty(StringUtils.trimToNull(directory));
+ boolean wildcardSet =
StringUtils.isNotEmpty(StringUtils.trimToNull(wildcard));
+ boolean excludeSet =
StringUtils.isNotEmpty(StringUtils.trimToNull(excludeWildcard));
+ if (!directorySet && (wildcardSet || excludeSet || includeSubFolders)) {
+ throw new HopException(
+ "Specify --environment-config-file-directory when using a
configuration file wildcard or --environment-config-include-subfolders");
+ }
+
+ List<String> files = new ArrayList<>();
+ if (explicitFiles != null) {
+ for (String explicitFile : explicitFiles) {
+ if (StringUtils.isEmpty(StringUtils.trimToNull(explicitFile))) {
+ continue;
+ }
+ String trimmed = explicitFile.trim();
+ if (!containsSameFile(variables, files, trimmed)) {
+ files.add(trimmed);
+ }
+ }
+ }
+ if (!directorySet) {
+ return files;
+ }
+
+ for (String matchedFile :
+ resolve(variables, directory.trim(), wildcard, excludeWildcard,
includeSubFolders)) {
+ if (!containsSameFile(variables, files, matchedFile)) {
+ files.add(matchedFile);
+ }
+ }
+ return files;
+ }
+
+ /**
+ * @param fileOrDirectory one configuration file, or a folder to scan
+ * @param wildcard regular expression matched against the file name, or
empty for every file
+ * @param excludeWildcard regular expression for file names to skip, or empty
+ * @param includeSubFolders also return matches from subfolders
+ * @return existing file paths, in {@link FileInputList} order
+ * @throws HopException when the location is missing, the expression is
invalid, or nothing
+ * matches
+ */
+ public static List<String> resolve(
+ IVariables variables,
+ String fileOrDirectory,
+ String wildcard,
+ String excludeWildcard,
+ boolean includeSubFolders)
+ throws HopException {
+ if (StringUtils.isEmpty(StringUtils.trimToNull(fileOrDirectory))) {
+ return List.of();
+ }
+
+ IVariables space = variables == null ? new Variables() : variables;
+ String location = fileOrDirectory.trim();
+ String mask = StringUtils.trimToEmpty(wildcard);
+ String exclude = StringUtils.trimToEmpty(excludeWildcard);
+ validatePattern(space.resolve(mask), "wildcard");
+ validatePattern(space.resolve(exclude), "exclude wildcard");
+
+ InputFile inputFile = new InputFile();
+ inputFile.setFileName(location);
+ inputFile.setFileMask(mask);
+ inputFile.setExcludeFileMask(exclude);
+ inputFile.setIncludeSubFolders(includeSubFolders);
+ inputFile.setFileRequired(true);
+ inputFile.setFileTypeFilter(FileTypeFilter.ONLY_FILES);
+
+ FileInputList fileInputList = FileInputList.createFileList(space,
List.of(inputFile));
+ if (fileInputList.nrOfFiles() == 0) {
+ throw new HopException(
+ "No configuration files matched '"
+ + location
+ + "'. The file or folder must exist. A wildcard is a regular
expression matched against the file name, for example .*\\.json");
+ }
+ List<String> paths = new ArrayList<>();
+ for (String path : fileInputList.getFileStrings()) {
+ paths.add(path);
+ }
+ return paths;
+ }
+
+ private static void validatePattern(String pattern, String label) throws
HopException {
+ if (StringUtils.isEmpty(pattern)) {
+ return;
+ }
+ try {
+ Pattern.compile(pattern);
+ } catch (PatternSyntaxException e) {
+ throw new HopException(
+ "Invalid "
+ + label
+ + " regular expression '"
+ + pattern
+ + "'. Use a regular expression such as .*\\.json, not a shell
glob such as *.json",
+ e);
+ }
+ }
+
+ private static boolean containsSameFile(
+ IVariables variables, List<String> files, String candidate) {
+ for (String file : files) {
+ if (sameFile(variables, file, candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean sameFile(IVariables variables, String left, String
right) {
+ if (StringUtils.equals(left, right)) {
+ return true;
+ }
+ try {
+ IVariables space = variables == null ? new Variables() : variables;
+ String leftName =
HopVfs.getFilename(HopVfs.getFileObject(space.resolve(left), space));
+ String rightName =
HopVfs.getFilename(HopVfs.getFileObject(space.resolve(right), space));
+ return leftName.equals(rightName);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
index a120bac114..ed6c69c8a2 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
@@ -38,6 +38,7 @@ import org.apache.hop.projects.config.ProjectsConfig;
import org.apache.hop.projects.config.ProjectsConfigSingleton;
import org.apache.hop.projects.project.ProjectConfig;
import org.apache.hop.projects.util.Defaults;
+import org.apache.hop.projects.util.PathVariableReplacer;
import org.apache.hop.ui.core.ConstUi;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.AttributesDialogExtension;
@@ -68,6 +69,7 @@ import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableItem;
@@ -96,6 +98,11 @@ public class LifecycleEnvironmentDialog extends Dialog {
private Text wCanvasText;
private TableView wConfigFiles;
+ private TextVar wAddFilename;
+ private TextVar wAddWildcard;
+ private TextVar wAddExcludeWildcard;
+ private Button wAddIncludeSubfolders;
+
private IVariables variables;
private Button wbEdit;
private Button wbImportVariables;
@@ -331,6 +338,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
comp.setLayout(layout);
tab.setControl(comp);
+ Group wAddGroup = createAddFilesGroup(comp, margin);
+
Label wlConfigFiles = new Label(comp, SWT.LEFT);
PropsUi.setLook(wlConfigFiles);
wlConfigFiles.setText(
@@ -338,7 +347,7 @@ public class LifecycleEnvironmentDialog extends Dialog {
FormData fdlConfigFiles = new FormData();
fdlConfigFiles.left = new FormAttachment(0, 0);
fdlConfigFiles.right = new FormAttachment(100, 0);
- fdlConfigFiles.top = new FormAttachment(0, 0);
+ fdlConfigFiles.top = new FormAttachment(wAddGroup, margin);
wlConfigFiles.setLayoutData(fdlConfigFiles);
wbImportVariables = new Button(comp, SWT.PUSH);
@@ -354,6 +363,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
Button wbSelect = new Button(comp, SWT.PUSH);
PropsUi.setLook(wbSelect);
wbSelect.setText(BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.Button.Select"));
+ wbSelect.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.Button.Select.Tooltip"));
FormData fdAdd = new FormData();
fdAdd.left = new FormAttachment(wbImportVariables, 0, SWT.LEFT);
fdAdd.right = new FormAttachment(100, 0);
@@ -467,25 +478,257 @@ public class LifecycleEnvironmentDialog extends Dialog {
}
}
+ private Group createAddFilesGroup(Composite parent, int margin) {
+ int middle = props.getMiddlePct();
+
+ Group group = new Group(parent, SWT.SHADOW_NONE);
+ PropsUi.setLook(group);
+ group.setText(BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Group"));
+ FormLayout groupLayout = new FormLayout();
+ groupLayout.marginWidth = PropsUi.getFormMargin();
+ groupLayout.marginHeight = PropsUi.getFormMargin();
+ group.setLayout(groupLayout);
+ FormData fdGroup = new FormData();
+ fdGroup.left = new FormAttachment(0, 0);
+ fdGroup.right = new FormAttachment(100, 0);
+ fdGroup.top = new FormAttachment(0, 0);
+ group.setLayoutData(fdGroup);
+
+ Button wBrowse = new Button(group, SWT.PUSH);
+ PropsUi.setLook(wBrowse);
+ wBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse"));
+ wBrowse.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Browse.Tooltip"));
+ FormData fdBrowse = new FormData();
+ fdBrowse.right = new FormAttachment(100, 0);
+ fdBrowse.top = new FormAttachment(0, margin);
+ wBrowse.setLayoutData(fdBrowse);
+ wBrowse.addListener(SWT.Selection, this::browseConfigFileOrDirectory);
+
+ Button wAdd = new Button(group, SWT.PUSH);
+ PropsUi.setLook(wAdd);
+ wAdd.setText(BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Button.Add"));
+ wAdd.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Button.Add.Tooltip"));
+ FormData fdAdd = new FormData();
+ fdAdd.right = new FormAttachment(wBrowse, -margin);
+ fdAdd.top = new FormAttachment(wBrowse, 0, SWT.CENTER);
+ wAdd.setLayoutData(fdAdd);
+ wAdd.addListener(SWT.Selection, this::addMatchingConfigFiles);
+
+ Label wlFilename = new Label(group, SWT.RIGHT);
+ PropsUi.setLook(wlFilename);
+ wlFilename.setText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Filename.Label"));
+ FormData fdlFilename = new FormData();
+ fdlFilename.left = new FormAttachment(0, 0);
+ fdlFilename.right = new FormAttachment(middle, -margin);
+ fdlFilename.top = new FormAttachment(wBrowse, 0, SWT.CENTER);
+ wlFilename.setLayoutData(fdlFilename);
+
+ wAddFilename =
+ new TextVar(variables, group, SWT.SINGLE | SWT.LEFT | SWT.BORDER)
+ .enableNamingSchemes(NamingSchemeTypes.FILE);
+ PropsUi.setLook(wAddFilename);
+ wAddFilename.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Filename.Tooltip"));
+ FormData fdFilename = new FormData();
+ fdFilename.left = new FormAttachment(middle, 0);
+ fdFilename.right = new FormAttachment(wAdd, -margin);
+ fdFilename.top = new FormAttachment(wlFilename, 0, SWT.CENTER);
+ wAddFilename.setLayoutData(fdFilename);
+
+ Label wlWildcard = new Label(group, SWT.RIGHT);
+ PropsUi.setLook(wlWildcard);
+ wlWildcard.setText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Wildcard.Label"));
+ FormData fdlWildcard = new FormData();
+ fdlWildcard.left = new FormAttachment(0, 0);
+ fdlWildcard.right = new FormAttachment(middle, -margin);
+ fdlWildcard.top = new FormAttachment(wAddFilename, margin);
+ wlWildcard.setLayoutData(fdlWildcard);
+
+ wAddWildcard = new TextVar(variables, group, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wAddWildcard);
+ wAddWildcard.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Wildcard.Tooltip"));
+ FormData fdWildcard = new FormData();
+ fdWildcard.left = new FormAttachment(middle, 0);
+ fdWildcard.right = new FormAttachment(wAddFilename, 0, SWT.RIGHT);
+ fdWildcard.top = new FormAttachment(wlWildcard, 0, SWT.CENTER);
+ wAddWildcard.setLayoutData(fdWildcard);
+
+ Label wlExcludeWildcard = new Label(group, SWT.RIGHT);
+ PropsUi.setLook(wlExcludeWildcard);
+ wlExcludeWildcard.setText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Label"));
+ FormData fdlExcludeWildcard = new FormData();
+ fdlExcludeWildcard.left = new FormAttachment(0, 0);
+ fdlExcludeWildcard.right = new FormAttachment(middle, -margin);
+ fdlExcludeWildcard.top = new FormAttachment(wAddWildcard, margin);
+ wlExcludeWildcard.setLayoutData(fdlExcludeWildcard);
+
+ wAddExcludeWildcard = new TextVar(variables, group, SWT.SINGLE | SWT.LEFT
| SWT.BORDER);
+ PropsUi.setLook(wAddExcludeWildcard);
+ wAddExcludeWildcard.setToolTipText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Tooltip"));
+ FormData fdExcludeWildcard = new FormData();
+ fdExcludeWildcard.left = new FormAttachment(middle, 0);
+ fdExcludeWildcard.right = new FormAttachment(wAddFilename, 0, SWT.RIGHT);
+ fdExcludeWildcard.top = new FormAttachment(wlExcludeWildcard, 0,
SWT.CENTER);
+ wAddExcludeWildcard.setLayoutData(fdExcludeWildcard);
+
+ wAddIncludeSubfolders = new Button(group, SWT.CHECK);
+ PropsUi.setLook(wAddIncludeSubfolders);
+ wAddIncludeSubfolders.setText(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Label"));
+ wAddIncludeSubfolders.setToolTipText(
+ BaseMessages.getString(
+ PKG,
"LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Tooltip"));
+ FormData fdIncludeSubfolders = new FormData();
+ fdIncludeSubfolders.left = new FormAttachment(middle, 0);
+ fdIncludeSubfolders.top = new FormAttachment(wAddExcludeWildcard, margin);
+ wAddIncludeSubfolders.setLayoutData(fdIncludeSubfolders);
+
+ return group;
+ }
+
+ private void browseConfigFileOrDirectory(Event event) {
+ boolean directory =
+ StringUtils.isNotEmpty(wAddWildcard.getText())
+ || StringUtils.isNotEmpty(wAddExcludeWildcard.getText())
+ || wAddIncludeSubfolders.getSelection();
+ if (directory) {
+ BaseDialog.presentDirectoryDialog(shell, wAddFilename, variables);
+ return;
+ }
+ BaseDialog.presentFileDialog(
+ shell,
+ wAddFilename,
+ variables,
+ new String[] {"*.json", "*"},
+ new String[] {
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.Json"),
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.All")
+ },
+ true);
+ }
+
+ private void addMatchingConfigFiles(Event event) {
+ try {
+ String location = wAddFilename.getText();
+ if (StringUtils.isEmpty(location)) {
+ showInformation(
+ "LifecycleEnvironmentDialog.AddFiles.MissingLocation.Title",
+ "LifecycleEnvironmentDialog.AddFiles.MissingLocation.Message");
+ return;
+ }
+ List<String> matches =
+ EnvironmentConfigFileSelector.resolve(
+ variables,
+ location,
+ wAddWildcard.getText(),
+ wAddExcludeWildcard.getText(),
+ wAddIncludeSubfolders.getSelection());
+ List<String> stored = new ArrayList<>();
+ for (String match : matches) {
+ stored.add(PathVariableReplacer.replacePathWithVariable(variables,
match));
+ }
+ if (appendConfigFiles(stored) == 0) {
+ showInformation(
+ "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title",
+ "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message");
+ return;
+ }
+ wAddFilename.setText("");
+ wAddWildcard.setText("");
+ wAddExcludeWildcard.setText("");
+ } catch (Exception e) {
+ new ErrorDialog(
+ shell,
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Error.Title"),
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.AddFiles.Error.Message"),
+ e);
+ }
+ }
+
private void addConfigFile(Event event) {
- String configFile =
- BaseDialog.presentFileDialog(
+ String[] configFiles =
+ BaseDialog.presentMultiFileDialog(
shell,
- null,
variables,
new String[] {"*.json", "*"},
- new String[] {"Config JSON files", "All files"},
+ new String[] {
+ BaseMessages.getString(
+ PKG,
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.Json"),
+ BaseMessages.getString(
+ PKG,
"LifecycleEnvironmentDialog.ImportVariables.FileFilter.All")
+ },
true);
- if (configFile != null) {
+ if (configFiles == null || configFiles.length == 0) {
+ return;
+ }
+ if (appendConfigFiles(List.of(configFiles)) == 0) {
+ showInformation(
+ "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title",
+ "LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message");
+ }
+ }
+
+ /** Adds paths that are not already listed and returns how many were added.
*/
+ private int appendConfigFiles(List<String> filenames) {
+ int added = 0;
+ TableItem last = null;
+ for (String filename : filenames) {
+ if (StringUtils.isEmpty(filename) || isConfigFileListed(filename)) {
+ continue;
+ }
TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
- item.setText(COL_FILENAME, configFile);
+ item.setText(COL_FILENAME, filename);
loadConfigFileIntoRow(item);
+ last = item;
+ added++;
+ }
+ if (added > 0) {
wConfigFiles.removeEmptyRows();
wConfigFiles.setRowNums();
wConfigFiles.optWidth(true);
- wConfigFiles.table.setSelection(item);
+ if (last != null) {
+ wConfigFiles.table.setSelection(last);
+ }
+ setButtonStates(null);
needingEnvironmentRefresh = true;
}
+ return added;
+ }
+
+ private boolean isConfigFileListed(String filename) {
+ String resolved = resolvedConfigFilename(filename);
+ for (TableItem item : wConfigFiles.getNonEmptyItems()) {
+ String existing = item.getText(COL_FILENAME);
+ if (filename.equals(existing) ||
resolved.equals(resolvedConfigFilename(existing))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String resolvedConfigFilename(String filename) {
+ if (StringUtils.isEmpty(filename) || variables == null) {
+ return Const.NVL(filename, "");
+ }
+ try {
+ return
HopVfs.getFilename(HopVfs.getFileObject(variables.resolve(filename),
variables));
+ } catch (Exception e) {
+ return variables.resolve(filename);
+ }
+ }
+
+ private void showInformation(String titleKey, String messageKey) {
+ MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
+ box.setText(BaseMessages.getString(PKG, titleKey));
+ box.setMessage(BaseMessages.getString(PKG, messageKey));
+ box.open();
}
private void newConfigFile(Event event) {
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
index 94a652e92d..c32fa4d6d4 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPlugin.java
@@ -17,8 +17,9 @@
package org.apache.hop.projects.environment;
-import java.util.Arrays;
import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.apache.hop.core.config.DescribedVariablesConfigFile;
import org.apache.hop.core.config.HopConfig;
@@ -72,6 +73,39 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
split = ",")
private String[] environmentConfigFiles;
+ @Getter
+ @Setter
+ @CommandLine.Option(
+ names = {"--environment-config-file-directory"},
+ description =
+ "Folder of configuration files for this lifecycle environment. "
+ + "Matching files replace the configuration file list, together
with --environment-config-files when that option is also set.")
+ private String environmentConfigFileDirectory;
+
+ @Getter
+ @Setter
+ @CommandLine.Option(
+ names = {"--environment-config-file-wildcard"},
+ description =
+ "Regular expression matched against file names in
--environment-config-file-directory. Example: .*\\.json")
+ private String environmentConfigFileWildcard;
+
+ @Getter
+ @Setter
+ @CommandLine.Option(
+ names = {"--environment-config-file-exclude-wildcard"},
+ description =
+ "Regular expression for file names to ignore in
--environment-config-file-directory. Example: .*secret.*")
+ private String environmentConfigFileExcludeWildcard;
+
+ @Getter
+ @Setter
+ @CommandLine.Option(
+ names = {"--environment-config-include-subfolders"},
+ description =
+ "Also include matching files from subfolders of
--environment-config-file-directory")
+ private boolean environmentConfigIncludeSubfolders;
+
@CommandLine.Option(
names = {"-em", "--environment-modify"},
description = "Modify a lifecycle environment")
@@ -164,7 +198,7 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
CONST_ENVIRONMENT + environmentName + "' doesn't exist, it can't be
modified");
}
- if (updateEnvironmentDetails(environment)) {
+ if (updateEnvironmentDetails(environment, variables)) {
config.addEnvironment(environment);
log.logBasic(
CONST_LIFECYCLE_ENVIRONMENT
@@ -197,7 +231,7 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
environment = new LifecycleEnvironment();
environment.setName(environmentName);
- updateEnvironmentDetails(environment);
+ updateEnvironmentDetails(environment, variables);
config.addEnvironment(environment);
ProjectsConfigSingleton.saveConfig();
@@ -243,7 +277,7 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
private void validateConfigFiles(
ILogChannel log, IVariables variables, LifecycleEnvironment environment)
throws Exception {
- if (environment == null || environmentConfigFiles == null) {
+ if (environment == null || !configurationFilesSpecified()) {
return;
}
for (String environmentConfigFilename :
environment.getConfigurationFiles()) {
@@ -260,7 +294,8 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
}
}
- private boolean updateEnvironmentDetails(LifecycleEnvironment environment) {
+ private boolean updateEnvironmentDetails(LifecycleEnvironment environment,
IVariables variables)
+ throws HopException {
boolean changed = false;
if (StringUtils.isNotEmpty(environmentPurpose)) {
environment.setPurpose(environmentPurpose);
@@ -270,14 +305,31 @@ public class ManageEnvironmentsOptionPlugin implements
IConfigOptions {
environment.setProjectName(environmentProject);
changed = true;
}
- if (environmentConfigFiles != null && environmentConfigFiles.length > 0) {
+ if (configurationFilesSpecified()) {
+ List<String> configurationFiles =
+ EnvironmentConfigFileSelector.combine(
+ variables,
+ environmentConfigFiles,
+ environmentConfigFileDirectory,
+ environmentConfigFileWildcard,
+ environmentConfigFileExcludeWildcard,
+ environmentConfigIncludeSubfolders);
environment.getConfigurationFiles().clear();
-
environment.getConfigurationFiles().addAll(Arrays.asList(environmentConfigFiles));
+ environment.getConfigurationFiles().addAll(configurationFiles);
changed = true;
}
return changed;
}
+ /** True when create/modify was asked to replace the configuration file
list. */
+ private boolean configurationFilesSpecified() {
+ return (environmentConfigFiles != null && environmentConfigFiles.length >
0)
+ || StringUtils.isNotEmpty(environmentConfigFileDirectory)
+ || StringUtils.isNotEmpty(environmentConfigFileWildcard)
+ || StringUtils.isNotEmpty(environmentConfigFileExcludeWildcard)
+ || environmentConfigIncludeSubfolders;
+ }
+
private void validateEnvironmentNameSpecified() throws Exception {
if (StringUtil.isEmpty(environmentName)) {
throw new HopException("Please specify the name of the environment to
create");
diff --git
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
index 250577c415..8c307e74e0 100644
---
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
+++
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties
@@ -19,6 +19,25 @@ LifecycleEnvironmentDialog.Button.Edit=Edit...
LifecycleEnvironmentDialog.Button.ImportVariables=Import variables
LifecycleEnvironmentDialog.Button.New=New...
LifecycleEnvironmentDialog.Button.Select=Select...
+LifecycleEnvironmentDialog.Button.Select.Tooltip=Select one or more existing
configuration files
+LifecycleEnvironmentDialog.AddFiles.Group=Add files
+LifecycleEnvironmentDialog.AddFiles.Filename.Label=File or directory
+LifecycleEnvironmentDialog.AddFiles.Filename.Tooltip=A configuration file, or
a folder to scan. Variables are supported, for example '${PROJECT_HOME}'.
+LifecycleEnvironmentDialog.AddFiles.Wildcard.Label=Inclusion wildcard (RegExp)
+LifecycleEnvironmentDialog.AddFiles.Wildcard.Tooltip=Regular expression
matched against each file name, for example '.*\\.json'. Leave empty to include
every file. This is not a shell glob.
+LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Label=Exclusion wildcard
(RegExp)
+LifecycleEnvironmentDialog.AddFiles.ExcludeWildcard.Tooltip=Regular expression
for file names to skip, for example '.*secret.*'.
+LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Label=Include subfolders
+LifecycleEnvironmentDialog.AddFiles.IncludeSubfolders.Tooltip=Also add
matching files from subfolders
+LifecycleEnvironmentDialog.AddFiles.Button.Add=Add
+LifecycleEnvironmentDialog.AddFiles.Button.Add.Tooltip=Add the file, or every
matching file in the folder, to the list below
+LifecycleEnvironmentDialog.AddFiles.Browse.Tooltip=Browse for a configuration
file, or for a folder when a wildcard or subfolders is set
+LifecycleEnvironmentDialog.AddFiles.MissingLocation.Title=File or directory
required
+LifecycleEnvironmentDialog.AddFiles.MissingLocation.Message=Specify a
configuration file or a directory before adding files.
+LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Title=Already listed
+LifecycleEnvironmentDialog.AddFiles.AlreadyListed.Message=Every matching
configuration file is already in the list.
+LifecycleEnvironmentDialog.AddFiles.Error.Title=Error
+LifecycleEnvironmentDialog.AddFiles.Error.Message=Error adding configuration
files
LifecycleEnvironmentDialog.DetailTable.Label.Filename=Filename
LifecycleEnvironmentDialog.DetailTable.Label.Description=Description
LifecycleEnvironmentDialog.DetailTable.ToolTip.Description=Short note stored
in the configuration file, next to its variables
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
new file mode 100644
index 0000000000..7b1d98fc67
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSelectorTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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.projects.environment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class EnvironmentConfigFileSelectorTest {
+
+ @TempDir Path tempDir;
+
+ @Test
+ void directoryWildcardKeepsJsonFilesOnly() throws Exception {
+ writeTree();
+
+ List<String> names =
+ baseNames(
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.toString(), ".*\\.json", "", false));
+
+ assertEquals(List.of("a.json", "notes.json", "secret.json"), names);
+ }
+
+ @Test
+ void excludeWildcardAndSubfolders() throws Exception {
+ writeTree();
+
+ List<String> withoutSubfolders =
+ baseNames(
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.toString(), ".*\\.json", "secret.*",
false));
+ assertEquals(List.of("a.json", "notes.json"), withoutSubfolders);
+
+ List<String> withSubfolders =
+ baseNames(
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.toString(), ".*\\.json", "secret.*",
true));
+ assertEquals(List.of("a.json", "nested.json", "notes.json"),
withSubfolders);
+ }
+
+ @Test
+ void singleFileAndVariableDirectory() throws Exception {
+ writeTree();
+ Path single = tempDir.resolve("a.json");
+ List<String> oneFile =
+ EnvironmentConfigFileSelector.resolve(new Variables(),
single.toString(), "", "", false);
+ assertEquals(1, oneFile.size());
+ assertTrue(oneFile.get(0).endsWith("a.json"));
+
+ IVariables variables = new Variables();
+ variables.setVariable("CONFIG_DIR", tempDir.toString());
+ List<String> fromVariable =
+ baseNames(
+ EnvironmentConfigFileSelector.resolve(
+ variables, "${CONFIG_DIR}", ".*\\.json", "", false));
+ assertEquals(List.of("a.json", "notes.json", "secret.json"), fromVariable);
+ }
+
+ @Test
+ void missingFolderAndShellGlobFail() throws Exception {
+ writeTree();
+
+ HopException missing =
+ assertThrows(
+ HopException.class,
+ () ->
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.resolve("missing").toString(),
"", "", false));
+ assertTrue(missing.getMessage().contains("missing"));
+
+ HopException glob =
+ assertThrows(
+ HopException.class,
+ () ->
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.toString(), "*.json", "", false));
+ assertTrue(glob.getMessage().contains("regular expression"));
+ }
+
+ @Test
+ void emptyLocationReturnsNothing() throws Exception {
+ assertTrue(
+ EnvironmentConfigFileSelector.resolve(new Variables(), " ",
".*\\.json", "", true)
+ .isEmpty());
+ }
+
+ @Test
+ void combineKeepsExplicitFilesAndSkipsDuplicates() throws Exception {
+ writeTree();
+ Path extra = tempDir.resolve("sibling").resolve("extra.json");
+ Files.createDirectories(extra.getParent());
+ Files.writeString(extra, "{}");
+
+ List<String> directoryFiles =
+ EnvironmentConfigFileSelector.resolve(
+ new Variables(), tempDir.toString(), ".*\\.json", "secret.*",
false);
+ assertEquals(2, directoryFiles.size());
+
+ List<String> combined =
+ EnvironmentConfigFileSelector.combine(
+ new Variables(),
+ new String[] {" ", extra.toString(), directoryFiles.get(0),
extra.toString()},
+ tempDir.toString(),
+ ".*\\.json",
+ "secret.*",
+ false);
+
+ assertEquals(3, combined.size());
+ assertEquals(extra.toString(), combined.get(0));
+ assertEquals(directoryFiles.get(0), combined.get(1));
+ assertEquals(directoryFiles.get(1), combined.get(2));
+ }
+
+ @Test
+ void wildcardWithoutDirectoryIsRejected() {
+ HopException exception =
+ assertThrows(
+ HopException.class,
+ () ->
+ EnvironmentConfigFileSelector.combine(
+ new Variables(), new String[] {"/tmp/a.json"}, null,
"*.json", null, false));
+
assertTrue(exception.getMessage().contains("--environment-config-file-directory"));
+
+ assertThrows(
+ HopException.class,
+ () -> EnvironmentConfigFileSelector.combine(new Variables(), null, "
", null, null, true));
+ }
+
+ @Test
+ void explicitFilesAloneAreNotScanned() throws Exception {
+ List<String> files =
+ EnvironmentConfigFileSelector.combine(
+ new Variables(),
+ new String[] {"config/a.json", " ", "config/b.json"},
+ null,
+ null,
+ null,
+ false);
+ assertEquals(List.of("config/a.json", "config/b.json"), files);
+ }
+
+ private void writeTree() throws Exception {
+ Files.writeString(tempDir.resolve("a.json"), "{}");
+ Files.writeString(tempDir.resolve("notes.json"), "{}");
+ Files.writeString(tempDir.resolve("secret.json"), "{}");
+ Files.writeString(tempDir.resolve("readme.txt"), "text");
+ Files.createDirectories(tempDir.resolve("nested"));
+ Files.writeString(tempDir.resolve("nested").resolve("nested.json"), "{}");
+ }
+
+ private static List<String> baseNames(List<String> paths) {
+ List<String> names = new ArrayList<>();
+ for (String path : paths) {
+ int slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
+ names.add(slash < 0 ? path : path.substring(slash + 1));
+ }
+ return names;
+ }
+}
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
new file mode 100644
index 0000000000..2995ac7ac3
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/ManageEnvironmentsOptionPluginTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.projects.environment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.projects.config.ProjectsConfigSingleton;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+class ManageEnvironmentsOptionPluginTest {
+
+ @TempDir Path tempDir;
+
+ @BeforeAll
+ static void beforeAll() {
+ HopLogStore.init();
+ }
+
+ @Test
+ void directoryOptionsAreParsed() {
+ ManageEnvironmentsOptionPlugin plugin = new
ManageEnvironmentsOptionPlugin();
+ new CommandLine(plugin)
+ .parseArgs(
+ "--environment-config-file-directory=/data/config",
+ "--environment-config-file-wildcard=.*\\.json",
+ "--environment-config-file-exclude-wildcard=.*secret.*",
+ "--environment-config-include-subfolders");
+
+ assertEquals("/data/config", plugin.getEnvironmentConfigFileDirectory());
+ assertEquals(".*\\.json", plugin.getEnvironmentConfigFileWildcard());
+ assertEquals(".*secret.*",
plugin.getEnvironmentConfigFileExcludeWildcard());
+ assertTrue(plugin.isEnvironmentConfigIncludeSubfolders());
+ }
+
+ @Test
+ void createEnvironmentStoresFilesFromADirectory() throws Exception {
+ Files.writeString(tempDir.resolve("a.json"), "{}");
+ Files.writeString(tempDir.resolve("notes.json"), "{}");
+ Files.writeString(tempDir.resolve("readme.txt"), "text");
+ Files.createDirectories(tempDir.resolve("nested"));
+ Files.writeString(tempDir.resolve("nested").resolve("nested.json"), "{}");
+
+ String name = "issue-4881-" + System.nanoTime();
+ HopConfig.setInMemoryMode(true);
+ try {
+ ManageEnvironmentsOptionPlugin plugin = new
ManageEnvironmentsOptionPlugin();
+ new CommandLine(plugin)
+ .parseArgs(
+ "--environment-create",
+ "--environment=" + name,
+ "--environment-project=missing-project",
+ "--environment-purpose=Testing",
+ "--environment-config-files=" + tempDir.resolve("notes.json"),
+ "--environment-config-file-directory=" + tempDir,
+ "--environment-config-file-wildcard=.*\\.json");
+
+ assertTrue(plugin.handleOption(LogChannel.GENERAL, null, new
Variables()));
+
+ LifecycleEnvironment environment =
ProjectsConfigSingleton.getConfig().findEnvironment(name);
+ List<String> files = environment.getConfigurationFiles();
+ assertEquals(2, files.size());
+ assertTrue(files.get(0).endsWith("notes.json"));
+ assertTrue(files.get(1).endsWith("a.json"));
+ } finally {
+ ProjectsConfigSingleton.getConfig().removeEnvironment(name);
+ HopConfig.setInMemoryMode(false);
+ }
+ }
+
+ @Test
+ void shellGlobDoesNotCreateTheEnvironment() throws Exception {
+ Files.writeString(tempDir.resolve("a.json"), "{}");
+ String name = "issue-4881-glob-" + System.nanoTime();
+ HopConfig.setInMemoryMode(true);
+ try {
+ ManageEnvironmentsOptionPlugin plugin = new
ManageEnvironmentsOptionPlugin();
+ new CommandLine(plugin)
+ .parseArgs(
+ "--environment-create",
+ "--environment=" + name,
+ "--environment-project=missing-project",
+ "--environment-config-file-directory=" + tempDir,
+ "--environment-config-file-wildcard=*.json");
+
+ HopException exception =
+ assertThrows(
+ HopException.class,
+ () -> plugin.handleOption(LogChannel.GENERAL, null, new
Variables()));
+ assertTrue(exception.getCause().getMessage().contains("regular
expression"));
+ assertNull(ProjectsConfigSingleton.getConfig().findEnvironment(name));
+ } finally {
+ ProjectsConfigSingleton.getConfig().removeEnvironment(name);
+ HopConfig.setInMemoryMode(false);
+ }
+ }
+}