This is an automated email from the ASF dual-hosted git repository.
hansva 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 8e0649b452 Issue #7906 : Add a description to environment
configuration files (#8579)
8e0649b452 is described below
commit 8e0649b4529de15e3ba04957cbaf9ccc0250faaa
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 25 10:44:28 2026 +0200
Issue #7906 : Add a description to environment configuration files (#8579)
Store an optional description next to the variables in an environment JSON
file, edit it from the environment dialog, and show the file's variables on
hover.
---
.../core/config/DescribedVariablesConfigFile.java | 32 +++
.../config/DescribedVariablesConfigFileTest.java | 90 ++++++++
.../ROOT/pages/projects/projects-environments.adoc | 29 ++-
.../environment/EnvironmentConfigFileSummary.java | 235 ++++++++++++++++++++
.../environment/LifecycleEnvironmentDialog.java | 189 ++++++++++++++--
.../environment/messages/messages_en_US.properties | 7 +
.../EnvironmentConfigFileSummaryTest.java | 244 +++++++++++++++++++++
.../projects/util/ProjectsConfigHelperTest.java | 7 +
.../core/dialog/HopDescribedVariablesDialog.java | 64 +++++-
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 5 +
.../core/dialog/messages/messages_en_US.properties | 2 +
11 files changed, 881 insertions(+), 23 deletions(-)
diff --git
a/core/src/main/java/org/apache/hop/core/config/DescribedVariablesConfigFile.java
b/core/src/main/java/org/apache/hop/core/config/DescribedVariablesConfigFile.java
index 69e4976f08..31d19d2b4f 100644
---
a/core/src/main/java/org/apache/hop/core/config/DescribedVariablesConfigFile.java
+++
b/core/src/main/java/org/apache/hop/core/config/DescribedVariablesConfigFile.java
@@ -20,6 +20,9 @@ package org.apache.hop.core.config;
import org.apache.hop.core.config.plugin.ConfigFile;
public class DescribedVariablesConfigFile extends ConfigFile implements
IConfigFile {
+ /** Optional note stored next to {@code variables} in an environment
configuration file. */
+ public static final String HOP_DESCRIPTION_KEY = "description";
+
private String configFilename;
public DescribedVariablesConfigFile(String configFilename) {
@@ -45,4 +48,33 @@ public class DescribedVariablesConfigFile extends ConfigFile
implements IConfigF
public void setConfigFilename(String configFilename) {
this.configFilename = configFilename;
}
+
+ /**
+ * The file description, or null when the file has none.
+ *
+ * <p>Blank values are treated as absent. This is the description of the
file itself, not the
+ * description of a variable inside it.
+ */
+ public String getDescription() {
+ Object value = configMap.get(HOP_DESCRIPTION_KEY);
+ if (value == null) {
+ return null;
+ }
+ String text = value.toString().trim();
+ return text.isEmpty() ? null : text;
+ }
+
+ /**
+ * Store or clear the file description.
+ *
+ * <p>A null or blank value removes the key so a file that never had a
description is not given an
+ * empty one the next time it is written.
+ */
+ public void setDescription(String description) {
+ if (description == null || description.trim().isEmpty()) {
+ configMap.remove(HOP_DESCRIPTION_KEY);
+ } else {
+ configMap.put(HOP_DESCRIPTION_KEY, description.trim());
+ }
+ }
}
diff --git
a/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
b/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
index ee94d989e2..0452ab1ed7 100644
---
a/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
+++
b/core/src/test/java/org/apache/hop/core/config/DescribedVariablesConfigFileTest.java
@@ -19,11 +19,18 @@ package org.apache.hop.core.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.hop.core.variables.DescribedVariable;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
class DescribedVariablesConfigFileTest {
+ @TempDir Path folder;
+
@Test
void filenameAndSerializer() {
DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile("/tmp/hop-config.json");
@@ -33,4 +40,87 @@ class DescribedVariablesConfigFileTest {
file.setConfigFilename("/other/path.json");
assertEquals("/other/path.json", file.getConfigFilename());
}
+
+ @Test
+ void descriptionRoundTripsWithVariables() throws Exception {
+ Path path = folder.resolve("env.json");
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ file.setDescription(" Warehouse connection ");
+ file.setDescribedVariable(new DescribedVariable("DB_HOSTNAME",
"localhost", "Warehouse host"));
+ file.saveToFile();
+
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(path.toString());
+ loaded.readFromFile();
+ assertEquals("Warehouse connection", loaded.getDescription());
+ assertEquals("localhost",
loaded.findDescribedVariableValue("DB_HOSTNAME"));
+ assertEquals("Warehouse host",
loaded.findDescribedVariable("DB_HOSTNAME").getDescription());
+ }
+
+ @Test
+ void readingVariablesKeepsTheFileDescription() throws Exception {
+ Path path = folder.resolve("env.json");
+ Files.writeString(
+ path,
+ """
+ {
+ "description" : "my environment config",
+ "variables" : [ { "name" : "TEST_ENV_VAR", "value" : "test_val",
"description" : "" } ]
+ }
+ """);
+
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ file.readFromFile();
+ assertEquals("test_val", file.findDescribedVariableValue("TEST_ENV_VAR"));
+ assertEquals(1, file.getDescribedVariables().size());
+ assertEquals("my environment config", file.getDescription());
+
+ file.saveToFile();
+ DescribedVariablesConfigFile reloaded = new
DescribedVariablesConfigFile(path.toString());
+ reloaded.readFromFile();
+ assertEquals("my environment config", reloaded.getDescription());
+ assertEquals("test_val",
reloaded.findDescribedVariableValue("TEST_ENV_VAR"));
+ }
+
+ @Test
+ void savingVariablesDoesNotInventADescription() throws Exception {
+ Path path = folder.resolve("env.json");
+ Files.writeString(
+ path,
+ """
+ { "extra" : "keep", "variables" : [ { "name" : "A", "value" : "1",
"description" : "d" } ] }
+ """);
+
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ file.readFromFile();
+ file.setDescribedVariables(file.getDescribedVariables());
+ file.saveToFile();
+
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(path.toString());
+ loaded.readFromFile();
+ assertNull(loaded.getDescription());
+
assertNull(loaded.getConfigMap().get(DescribedVariablesConfigFile.HOP_DESCRIPTION_KEY));
+ assertEquals("keep", loaded.getConfigMap().get("extra"));
+ assertEquals("1", loaded.findDescribedVariableValue("A"));
+ assertEquals("d", loaded.findDescribedVariable("A").getDescription());
+ }
+
+ @Test
+ void blankDescriptionRemovesTheKeyAndKeepsOtherEntries() throws Exception {
+ Path path = folder.resolve("env.json");
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ file.setDescription("hello");
+ file.setDescribedVariable(new DescribedVariable("A", "1", ""));
+ file.getConfigMap().put("extra", "keep");
+ file.saveToFile();
+
+ file.setDescription(" ");
+ file.saveToFile();
+
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(path.toString());
+ loaded.readFromFile();
+ assertNull(loaded.getDescription());
+
assertNull(loaded.getConfigMap().get(DescribedVariablesConfigFile.HOP_DESCRIPTION_KEY));
+ assertEquals("keep", loaded.getConfigMap().get("extra"));
+ assertEquals("1", loaded.findDescribedVariableValue("A"));
+ }
}
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 a0e001764b..9af778be4a 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
@@ -143,11 +143,12 @@
image:hop-gui/environment/environment-dialog-configuration-files-tab.png[Environ
[id="tab-env-props-files",cols="20%,45%,5%,5%,25%",options="header"]
|===
|Property|Description|Variables Supported|Mandatory|Default
-|Configuration files|One or more files that define environment-specific
variables|Yes|No|
+|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|
|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|
|New...|Create a new configuration file and add it to the environment|No|No|
-|Edit...|Open the selected configuration file to set variable names, values
and descriptions|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.
@@ -159,10 +160,32 @@ See xref:best-practices/index.adoc[Best practices:
security] and xref:hop-gui/pe
image:hop-gui/environment/environment-files.png[Environment configuration
files,width="90%"]
-*Edit...* opens the described variables dialog for the selected file:
+*Edit...* opens the described variables dialog for the selected file.
+The description at the top of that dialog is the same file description as the
*Description* column.
image::hop-gui/environment/environment-variables.png[Environment
Variables,width="80%"]
+The description is written into the configuration file when you confirm the
environment dialog.
+A file whose description did not change is left as it is.
+A file that does not exist yet is created when the description is not empty.
+Hover a row to see that description and the variables stored in the file.
+Values are shown as they are written, not with variables resolved.
+The value is hidden when the variable name contains `password`, `passwd`,
`secret`, `token` or `credential`, and when the value is an encrypted Hop
password.
+
+A configuration file looks like this:
+
+[source,json]
+----
+{
+ "description" : "Warehouse connection for this environment",
+ "variables" : [ {
+ "name" : "DB_HOSTNAME",
+ "value" : "db.example.com",
+ "description" : "Warehouse host"
+ } ]
+}
+----
+
When you browse for a file or directory in Hop Gui with a project active,
paths under a matching path-like variable are rewritten automatically. For
example, `+${PROJECT_HOME}+` is used for files under the project home, and an
environment variable such as `SOURCE_FILES=/data/incoming` (or
`SOURCE_FILES=~/incoming`) rewrites a selection under that folder to
`+${SOURCE_FILES}/…+`. If more than one variable matches, the longest (most
specific) path wins. The same applies to path variables d [...]
After creating an environment the user interface will switch to it.
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummary.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummary.java
new file mode 100644
index 0000000000..99ca327e03
--- /dev/null
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummary.java
@@ -0,0 +1,235 @@
+/*
+ * 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 org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.config.DescribedVariablesConfigFile;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.DescribedVariable;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+
+/**
+ * Reads an environment configuration file for the environment dialog: its
description, the
+ * variables to show on hover, and a description-only save that leaves the
rest of the file alone.
+ */
+public final class EnvironmentConfigFileSummary {
+ private static final Class<?> PKG = EnvironmentConfigFileSummary.class;
+
+ public static final int MAX_VARIABLES_IN_TOOLTIP = 40;
+ public static final String MASKED_VALUE = "********";
+
+ private static final Pattern SECRET_NAME =
+ Pattern.compile("(?i).*(password|passwd|secret|token|credential).*");
+
+ private enum State {
+ OK,
+ MISSING,
+ UNREADABLE
+ }
+
+ private final String description;
+ private final List<DescribedVariable> variables;
+ private final State state;
+
+ private EnvironmentConfigFileSummary(
+ String description, List<DescribedVariable> variables, State state) {
+ this.description = description;
+ this.variables = variables;
+ this.state = state;
+ }
+
+ /**
+ * Read the configuration file at a resolved path.
+ *
+ * @param resolvedFilename absolute or local path with variables already
resolved. Empty means the
+ * row has no file yet.
+ * @return a summary. A missing or unreadable file does not throw.
+ */
+ public static EnvironmentConfigFileSummary read(String resolvedFilename) {
+ if (StringUtils.isEmpty(resolvedFilename)) {
+ return new EnvironmentConfigFileSummary(null, List.of(), State.MISSING);
+ }
+ try {
+ if (!HopVfs.fileExists(resolvedFilename)) {
+ return new EnvironmentConfigFileSummary(null, List.of(),
State.MISSING);
+ }
+ } catch (Exception e) {
+ return new EnvironmentConfigFileSummary(null, List.of(),
State.UNREADABLE);
+ }
+ try {
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(resolvedFilename);
+ file.readFromFile();
+ // Description first: getDescribedVariables() can replace the map with a
nested "config".
+ String description = file.getDescription();
+ return new EnvironmentConfigFileSummary(description,
copyVariables(file), State.OK);
+ } catch (Exception e) {
+ return new EnvironmentConfigFileSummary(null, List.of(),
State.UNREADABLE);
+ }
+ }
+
+ /**
+ * @return the description stored in the file, or null when there is none or
the file was not read
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Hover text for one configuration-file row.
+ *
+ * @param descriptionFromCell the description currently shown in the table,
which may not be saved
+ * yet. The stored variable values are shown as written; they are not
resolved.
+ */
+ public String tooltip(String descriptionFromCell) {
+ List<String> lines = new ArrayList<>();
+ String descriptionText = normalize(descriptionFromCell);
+ if (!descriptionText.isEmpty()) {
+ lines.add(descriptionText);
+ }
+
+ List<String> body = new ArrayList<>();
+ if (state == State.MISSING) {
+
body.add(message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.Missing"));
+ } else if (state == State.UNREADABLE) {
+
body.add(message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.Unreadable"));
+ } else if (variables.isEmpty()) {
+
body.add(message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.NoVariables"));
+ } else {
+ int limit = Math.min(MAX_VARIABLES_IN_TOOLTIP, variables.size());
+ for (int i = 0; i < limit; i++) {
+ body.add(formatVariable(variables.get(i)));
+ }
+ if (variables.size() > MAX_VARIABLES_IN_TOOLTIP) {
+ body.add(
+ BaseMessages.getString(
+ PKG,
+ "LifecycleEnvironmentDialog.ConfigFile.ToolTip.More",
+ Integer.toString(variables.size() -
MAX_VARIABLES_IN_TOOLTIP)));
+ }
+ }
+
+ if (!body.isEmpty()) {
+ if (!lines.isEmpty()) {
+ lines.add("");
+ }
+ lines.addAll(body);
+ }
+ return String.join("\n", lines);
+ }
+
+ /**
+ * Write {@code cellDescription} into the file when it differs from what is
already stored.
+ *
+ * <p>An unchanged description does not rewrite the file. A missing file is
created only when the
+ * description is not blank, with an empty variable list. A file that exists
but cannot be read is
+ * left alone; a non-blank new description is an error so the dialog can
stay open.
+ *
+ * @return true when the file was written
+ */
+ public static boolean saveDescriptionIfChanged(String resolvedFilename,
String cellDescription)
+ throws HopException {
+ if (StringUtils.isEmpty(resolvedFilename)) {
+ return false;
+ }
+ String requested = normalize(cellDescription);
+ boolean exists;
+ try {
+ exists = HopVfs.fileExists(resolvedFilename);
+ } catch (Exception e) {
+ if (requested.isEmpty()) {
+ return false;
+ }
+ throw cannotRead(resolvedFilename, e);
+ }
+
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(resolvedFilename);
+ if (exists) {
+ try {
+ file.readFromFile();
+ } catch (Exception e) {
+ if (requested.isEmpty()) {
+ return false;
+ }
+ throw cannotRead(resolvedFilename, e);
+ }
+ // Compare without calling getDescribedVariables(), which rewrites the
variable list.
+ if (requested.equals(normalize(file.getDescription()))) {
+ return false;
+ }
+ file.setDescription(requested);
+ file.saveToFile();
+ return true;
+ }
+
+ if (requested.isEmpty()) {
+ return false;
+ }
+ file.setDescription(requested);
+ file.setDescribedVariables(new ArrayList<>());
+ file.saveToFile();
+ return true;
+ }
+
+ private static HopException cannotRead(String resolvedFilename, Exception
cause) {
+ return new HopException(
+ "Could not read configuration file '" + resolvedFilename + "' to
update its description",
+ cause);
+ }
+
+ private static List<DescribedVariable>
copyVariables(DescribedVariablesConfigFile file) {
+ List<DescribedVariable> copy = new ArrayList<>();
+ for (DescribedVariable variable : file.getDescribedVariables()) {
+ if (variable != null && StringUtils.isNotBlank(variable.getName())) {
+ copy.add(new DescribedVariable(variable));
+ }
+ }
+ return copy;
+ }
+
+ private static String formatVariable(DescribedVariable variable) {
+ String value = maskValue(variable) ? MASKED_VALUE :
Const.NVL(variable.getValue(), "");
+ String line = variable.getName() + " = " + value;
+ if (StringUtils.isNotBlank(variable.getDescription())) {
+ line = line + " (" + variable.getDescription().trim() + ")";
+ }
+ return line;
+ }
+
+ private static boolean maskValue(DescribedVariable variable) {
+ String name = variable.getName();
+ if (name != null && SECRET_NAME.matcher(name).matches()) {
+ return true;
+ }
+ String value = variable.getValue();
+ return value != null && value.startsWith(Encr.PASSWORD_ENCRYPTED_PREFIX);
+ }
+
+ private static String message(String key) {
+ return BaseMessages.getString(PKG, key);
+ }
+
+ private static String normalize(String value) {
+ return value == null ? "" : value.trim();
+ }
+}
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 8c820c1c0c..a120bac114 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
@@ -18,6 +18,7 @@
package org.apache.hop.projects.environment;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -57,6 +58,8 @@ import org.apache.hop.ui.util.HelpUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
@@ -74,6 +77,12 @@ public class LifecycleEnvironmentDialog extends Dialog {
private static final Class<?> PKG = LifecycleEnvironmentDialog.class;
public static final String CONST_ERROR = "Error";
+ private static final int COL_FILENAME = 1;
+ private static final int COL_DESCRIPTION = 2;
+
+ /** Resolved path last read into this row, so leaving the filename cell does
not reload it. */
+ private static final String DATA_RESOLVED_FILENAME =
"resolvedConfigFilename";
+
private final LifecycleEnvironment environment;
private String returnValue;
@@ -108,6 +117,11 @@ public class LifecycleEnvironmentDialog extends Dialog {
/** Localized purpose label → fixed English suffix (for new-environment name
suggestion). */
private Map<String, String> knownPurposeSuffixes;
+ /**
+ * Resolved configuration-file path → last read, so a description edit does
not re-read the file.
+ */
+ private final Map<String, EnvironmentConfigFileSummary> configFileSummaries
= new HashMap<>();
+
public LifecycleEnvironmentDialog(
Shell parent, LifecycleEnvironment environment, IVariables variables) {
super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE);
@@ -347,16 +361,23 @@ public class LifecycleEnvironmentDialog extends Dialog {
wbSelect.setLayoutData(fdAdd);
wbSelect.addListener(SWT.Selection, this::addConfigFile);
- ColumnInfo[] columnInfo =
- new ColumnInfo[] {
- new ColumnInfo(
- BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.DetailTable.Label.Filename"),
- ColumnInfo.COLUMN_TYPE_TEXT,
- false,
- false),
- };
- columnInfo[0].setUsingVariables(true);
- columnInfo[0].setNamingSchemeType(NamingSchemeTypes.FILE);
+ ColumnInfo filenameColumn =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.DetailTable.Label.Filename"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false);
+ filenameColumn.setUsingVariables(true);
+ filenameColumn.setNamingSchemeType(NamingSchemeTypes.FILE);
+ ColumnInfo descriptionColumn =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.DetailTable.Label.Description"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false);
+ descriptionColumn.setToolTip(
+ BaseMessages.getString(PKG,
"LifecycleEnvironmentDialog.DetailTable.ToolTip.Description"));
+ ColumnInfo[] columnInfo = new ColumnInfo[] {filenameColumn,
descriptionColumn};
wConfigFiles =
new TableView(
@@ -368,6 +389,10 @@ public class LifecycleEnvironmentDialog extends Dialog {
null,
props);
PropsUi.setLook(wConfigFiles);
+ wConfigFiles.setContentListener(this::onConfigFileCellChanged);
+ // SWT 3.134 has no TableItem.setToolTipText. The table tooltip is set
from the row under the
+ // pointer. Clearing it first is what makes GTK replace the previous row's
text.
+ wConfigFiles.table.addListener(SWT.MouseHover, this::onConfigFileHover);
FormData fdConfigFiles = new FormData();
fdConfigFiles.left = new FormAttachment(0, 0);
fdConfigFiles.right = new FormAttachment(wbImportVariables, -2 * margin);
@@ -404,11 +429,12 @@ public class LifecycleEnvironmentDialog extends Dialog {
if (index < 0) {
return;
}
- String configFilename = wConfigFiles.getItem(index, 1);
+ String configFilename = wConfigFiles.getItem(index, COL_FILENAME);
if (StringUtils.isEmpty(configFilename)) {
return;
}
String realConfigFilename = variables.resolve(configFilename);
+ String cellDescription = wConfigFiles.getItem(index, COL_DESCRIPTION);
DescribedVariablesConfigFile variablesConfigFile =
new DescribedVariablesConfigFile(realConfigFilename);
@@ -425,10 +451,15 @@ public class LifecycleEnvironmentDialog extends Dialog {
} else {
variablesConfigFile.readFromFile();
}
+ // An unsaved description in the table has to win over the file, or Edit
would drop it.
+ if (!sameDescription(variablesConfigFile.getDescription(),
cellDescription)) {
+ variablesConfigFile.setDescription(cellDescription);
+ }
boolean changed = HopGui.editConfigFile(shell, realConfigFilename,
variablesConfigFile, null);
if (changed) {
needingEnvironmentRefresh = true;
+ loadConfigFileIntoRow(wConfigFiles.table.getItem(index));
}
} catch (Exception e) {
@@ -447,7 +478,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
true);
if (configFile != null) {
TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
- item.setText(1, configFile);
+ item.setText(COL_FILENAME, configFile);
+ loadConfigFileIntoRow(item);
wConfigFiles.removeEmptyRows();
wConfigFiles.setRowNums();
wConfigFiles.optWidth(true);
@@ -491,7 +523,8 @@ public class LifecycleEnvironmentDialog extends Dialog {
true);
if (configFile != null) {
TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
- item.setText(1, configFile);
+ item.setText(COL_FILENAME, configFile);
+ loadConfigFileIntoRow(item);
wConfigFiles.removeEmptyRows();
wConfigFiles.setRowNums();
wConfigFiles.optWidth(true);
@@ -538,7 +571,10 @@ public class LifecycleEnvironmentDialog extends Dialog {
List<String> configurationFiles = new ArrayList<>();
for (TableItem item : wConfigFiles.getNonEmptyItems()) {
- configurationFiles.add(item.getText(1));
+ String filename = item.getText(COL_FILENAME);
+ if (StringUtils.isNotEmpty(filename)) {
+ configurationFiles.add(filename);
+ }
}
List<DescribedVariable> proposed =
@@ -584,21 +620,28 @@ public class LifecycleEnvironmentDialog extends Dialog {
// Ensure the path is listed on the environment
boolean alreadyListed = false;
+ TableItem savedItem = null;
for (TableItem item : wConfigFiles.getNonEmptyItems()) {
- if (configFilename.equals(item.getText(1))
- || realConfigFilename.equals(variables.resolve(item.getText(1)))) {
+ if (configFilename.equals(item.getText(COL_FILENAME))
+ ||
realConfigFilename.equals(variables.resolve(item.getText(COL_FILENAME)))) {
alreadyListed = true;
+ savedItem = item;
break;
}
}
if (!alreadyListed) {
TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
- item.setText(1, configFilename);
+ item.setText(COL_FILENAME, configFilename);
+ savedItem = item;
wConfigFiles.removeEmptyRows();
wConfigFiles.setRowNums();
wConfigFiles.optWidth(true);
wConfigFiles.table.setSelection(item);
}
+ if (savedItem != null) {
+ // Keep a description the user typed and has not saved; the file now
has the new variables.
+ rereadConfigFileSummary(savedItem);
+ }
needingEnvironmentRefresh = true;
@@ -705,6 +748,7 @@ public class LifecycleEnvironmentDialog extends Dialog {
"Sorry, renaming environment '" + originalName + "' is not
supported.");
}
+ saveConfigFileDescriptions();
getInfo(environment);
if (dialogExtension != null) {
dialogExtension.getContext().setProjectName(environment.getProjectName());
@@ -807,10 +851,12 @@ public class LifecycleEnvironmentDialog extends Dialog {
}
wConfigFiles.table.removeAll();
+ configFileSummaries.clear();
for (int i = 0; i < environment.getConfigurationFiles().size(); i++) {
String configurationFile = environment.getConfigurationFiles().get(i);
TableItem item = new TableItem(wConfigFiles.table, SWT.NONE);
- item.setText(1, Const.NVL(configurationFile, ""));
+ item.setText(COL_FILENAME, Const.NVL(configurationFile, ""));
+ loadConfigFileIntoRow(item);
}
if (environment.getConfigurationFiles().isEmpty()) {
new TableItem(wConfigFiles.table, SWT.NONE);
@@ -881,10 +927,115 @@ public class LifecycleEnvironmentDialog extends Dialog {
env.getConfigurationFiles().clear();
for (TableItem item : wConfigFiles.getNonEmptyItems()) {
- env.getConfigurationFiles().add(item.getText(1));
+ String filename = item.getText(COL_FILENAME);
+ if (StringUtils.isNotEmpty(filename)) {
+ env.getConfigurationFiles().add(filename);
+ }
+ }
+ }
+
+ /**
+ * SWT's {@code ModifyEvent} no longer carries the cell coordinates, so look
at every row. A
+ * description edit leaves the resolved path alone and is picked up on the
next hover.
+ */
+ private void onConfigFileCellChanged(ModifyEvent event) {
+ if (wConfigFiles == null || wConfigFiles.table.isDisposed()) {
+ return;
+ }
+ for (TableItem item : wConfigFiles.table.getItems()) {
+ if (item.isDisposed()) {
+ continue;
+ }
+ String filename = item.getText(COL_FILENAME);
+ String resolved = StringUtils.isEmpty(filename) ? "" :
variables.resolve(filename);
+ String previous = Const.NVL((String)
item.getData(DATA_RESOLVED_FILENAME), "");
+ if (!resolved.equals(previous)) {
+ loadConfigFileIntoRow(item);
+ }
+ }
+ }
+
+ private void onConfigFileHover(Event event) {
+ if (wConfigFiles == null || wConfigFiles.table.isDisposed()) {
+ return;
+ }
+ TableItem item = wConfigFiles.table.getItem(new Point(event.x, event.y));
+ String tip = "";
+ if (item != null && !item.isDisposed() &&
StringUtils.isNotEmpty(item.getText(COL_FILENAME))) {
+ String resolved = variables.resolve(item.getText(COL_FILENAME));
+ EnvironmentConfigFileSummary summary = configFileSummaries.get(resolved);
+ if (summary == null) {
+ summary = EnvironmentConfigFileSummary.read(resolved);
+ configFileSummaries.put(resolved, summary);
+ item.setData(DATA_RESOLVED_FILENAME, resolved);
+ }
+ tip = summary.tooltip(item.getText(COL_DESCRIPTION));
+ }
+ wConfigFiles.table.setToolTipText("");
+ wConfigFiles.table.setToolTipText(tip);
+ }
+
+ /** Read the file named on the row and show its description. */
+ private void loadConfigFileIntoRow(TableItem item) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ String filename = item.getText(COL_FILENAME);
+ if (StringUtils.isEmpty(filename)) {
+ item.setData(DATA_RESOLVED_FILENAME, "");
+ return;
+ }
+ String resolved = variables.resolve(filename);
+ EnvironmentConfigFileSummary summary =
EnvironmentConfigFileSummary.read(resolved);
+ configFileSummaries.put(resolved, summary);
+ item.setData(DATA_RESOLVED_FILENAME, resolved);
+ item.setText(COL_DESCRIPTION, Const.NVL(summary.getDescription(), ""));
+ }
+
+ /** Re-read the file after its variables changed, keeping the description
typed in the cell. */
+ private void rereadConfigFileSummary(TableItem item) {
+ if (item == null || item.isDisposed()) {
+ return;
+ }
+ String filename = item.getText(COL_FILENAME);
+ if (StringUtils.isEmpty(filename)) {
+ item.setData(DATA_RESOLVED_FILENAME, "");
+ return;
+ }
+ String resolved = variables.resolve(filename);
+ configFileSummaries.remove(resolved);
+ EnvironmentConfigFileSummary summary =
EnvironmentConfigFileSummary.read(resolved);
+ configFileSummaries.put(resolved, summary);
+ item.setData(DATA_RESOLVED_FILENAME, resolved);
+ }
+
+ /**
+ * Write description edits back into the configuration files. A file whose
description did not
+ * change is not rewritten.
+ */
+ private void saveConfigFileDescriptions() throws HopException {
+ for (TableItem item : wConfigFiles.getNonEmptyItems()) {
+ String filename = item.getText(COL_FILENAME);
+ if (StringUtils.isEmpty(filename)) {
+ continue;
+ }
+ String resolved = variables.resolve(filename);
+ try {
+ EnvironmentConfigFileSummary.saveDescriptionIfChanged(
+ resolved, item.getText(COL_DESCRIPTION));
+ } catch (HopException e) {
+ throw new HopException(
+ BaseMessages.getString(
+ PKG,
"LifecycleEnvironmentDialog.ConfigFile.SaveDescription.Error", resolved),
+ e);
+ }
}
}
+ private static boolean sameDescription(String stored, String cell) {
+ return StringUtils.equals(StringUtils.trimToEmpty(stored),
StringUtils.trimToEmpty(cell));
+ }
+
/**
* Gets needingEnvironmentRefresh
*
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 041a367a3f..250577c415 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
@@ -20,6 +20,13 @@ LifecycleEnvironmentDialog.Button.ImportVariables=Import
variables
LifecycleEnvironmentDialog.Button.New=New...
LifecycleEnvironmentDialog.Button.Select=Select...
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
+LifecycleEnvironmentDialog.ConfigFile.ToolTip.Unreadable=Could not read this
configuration file.
+LifecycleEnvironmentDialog.ConfigFile.ToolTip.Missing=This configuration file
does not exist yet.
+LifecycleEnvironmentDialog.ConfigFile.ToolTip.NoVariables=No variables defined.
+LifecycleEnvironmentDialog.ConfigFile.ToolTip.More=\u2026 and {0} more
+LifecycleEnvironmentDialog.ConfigFile.SaveDescription.Error=Could not save the
description for configuration file:\n{0}
LifecycleEnvironmentDialog.Group.Label.ConfigurationFiles=Configuration files:
LifecycleEnvironmentDialog.Label.EnvironmentName=Name
LifecycleEnvironmentDialog.Label.EnvironmentPurpose=Purpose
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummaryTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummaryTest.java
new file mode 100644
index 0000000000..2e194d5614
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/environment/EnvironmentConfigFileSummaryTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.assertFalse;
+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 org.apache.hop.core.config.DescribedVariablesConfigFile;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.DescribedVariable;
+import org.apache.hop.i18n.BaseMessages;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class EnvironmentConfigFileSummaryTest {
+
+ @TempDir Path folder;
+
+ @Test
+ void tooltipShowsStoredValuesAndVariableDescriptions() throws Exception {
+ Path path = folder.resolve("env.json");
+ write(
+ path,
+ """
+ {
+ "description" : "Warehouse connection",
+ "variables" : [ {
+ "name" : "DB_HOSTNAME",
+ "value" : "${OTHER}",
+ "description" : "Warehouse host"
+ }, {
+ "name" : "DB_PORT",
+ "value" : "5432",
+ "description" : ""
+ } ]
+ }
+ """);
+
+ EnvironmentConfigFileSummary summary =
EnvironmentConfigFileSummary.read(path.toString());
+ assertEquals("Warehouse connection", summary.getDescription());
+ String tooltip = summary.tooltip(summary.getDescription());
+
+ assertTrue(tooltip.startsWith("Warehouse connection\n\n"));
+ assertTrue(tooltip.contains("DB_HOSTNAME = ${OTHER} (Warehouse host)"));
+ assertTrue(tooltip.contains("DB_PORT = 5432"));
+ assertFalse(tooltip.contains("resolved-host"));
+ }
+
+ @Test
+ void tooltipMasksSecretsAndEncryptedValues() throws Exception {
+ Path path = folder.resolve("secrets.json");
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ file.setDescribedVariable(new DescribedVariable("DB_PASSWORD", "s3cret",
""));
+ file.setDescribedVariable(new DescribedVariable("API_TOKEN", "abc",
"api"));
+ file.setDescribedVariable(new DescribedVariable("my_secret", "hide-me",
""));
+ file.setDescribedVariable(new DescribedVariable("credential_file",
"hide-too", ""));
+ file.setDescribedVariable(new DescribedVariable("DB_PASSWD",
"hide-passwd", ""));
+ file.setDescribedVariable(
+ new DescribedVariable("DB_HOSTNAME", Encr.PASSWORD_ENCRYPTED_PREFIX +
"xyz", "host"));
+ file.setDescribedVariable(new DescribedVariable("PLAIN", "visible", ""));
+ file.saveToFile();
+
+ String tooltip =
EnvironmentConfigFileSummary.read(path.toString()).tooltip(null);
+
+ assertTrue(tooltip.contains("DB_PASSWORD = " +
EnvironmentConfigFileSummary.MASKED_VALUE));
+ assertTrue(
+ tooltip.contains("API_TOKEN = " +
EnvironmentConfigFileSummary.MASKED_VALUE + " (api)"));
+ assertTrue(tooltip.contains("my_secret = " +
EnvironmentConfigFileSummary.MASKED_VALUE));
+ assertTrue(tooltip.contains("credential_file = " +
EnvironmentConfigFileSummary.MASKED_VALUE));
+ assertTrue(tooltip.contains("DB_PASSWD = " +
EnvironmentConfigFileSummary.MASKED_VALUE));
+ assertTrue(
+ tooltip.contains("DB_HOSTNAME = " +
EnvironmentConfigFileSummary.MASKED_VALUE + " (host)"));
+ assertTrue(tooltip.contains("PLAIN = visible"));
+ assertFalse(tooltip.contains("s3cret"));
+ assertFalse(tooltip.contains("hide-me"));
+ assertFalse(tooltip.contains(Encr.PASSWORD_ENCRYPTED_PREFIX + "xyz"));
+ }
+
+ @Test
+ void tooltipCapsTheVariableList() throws Exception {
+ Path path = folder.resolve("many.json");
+ DescribedVariablesConfigFile file = new
DescribedVariablesConfigFile(path.toString());
+ int count = EnvironmentConfigFileSummary.MAX_VARIABLES_IN_TOOLTIP + 1;
+ for (int i = 1; i <= count; i++) {
+ file.setDescribedVariable(new
DescribedVariable(String.format("NAME_%02d", i), "v" + i, ""));
+ }
+ file.saveToFile();
+
+ String tooltip =
EnvironmentConfigFileSummary.read(path.toString()).tooltip("Many");
+ assertTrue(tooltip.contains("NAME_01 = v1"));
+ assertTrue(tooltip.contains("NAME_40 = v40"));
+ assertFalse(tooltip.contains("NAME_41 ="));
+ assertTrue(
+ tooltip.contains(
+ BaseMessages.getString(
+ EnvironmentConfigFileSummary.class,
+ "LifecycleEnvironmentDialog.ConfigFile.ToolTip.More",
+ "1")));
+ }
+
+ @Test
+ void tooltipForMissingUnreadableAndEmptyFiles() throws Exception {
+ Path missing = folder.resolve("missing.json");
+ EnvironmentConfigFileSummary missingSummary =
+ EnvironmentConfigFileSummary.read(missing.toString());
+ assertNull(missingSummary.getDescription());
+ assertEquals(
+ message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.Missing"),
+ missingSummary.tooltip(null));
+ assertEquals(
+ "Note\n\n" +
message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.Missing"),
+ missingSummary.tooltip(" Note "));
+
+ Path broken = folder.resolve("broken.json");
+ Files.writeString(broken, "{");
+ EnvironmentConfigFileSummary brokenSummary =
+ EnvironmentConfigFileSummary.read(broken.toString());
+ assertEquals(
+ message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.Unreadable"),
+ brokenSummary.tooltip(""));
+
+ Path empty = folder.resolve("empty.json");
+ Files.writeString(empty, "{ \"description\" : \"Only a note\",
\"variables\" : [ ] }");
+ EnvironmentConfigFileSummary emptySummary =
EnvironmentConfigFileSummary.read(empty.toString());
+ assertEquals(
+ "Only a note\n\n" +
message("LifecycleEnvironmentDialog.ConfigFile.ToolTip.NoVariables"),
+ emptySummary.tooltip(emptySummary.getDescription()));
+ }
+
+ @Test
+ void unchangedDescriptionDoesNotRewriteTheFile() throws Exception {
+ Path path = folder.resolve("same.json");
+ String original =
+ """
+ {
+ "description" : "keep",
+ "variables" : [ {
+ "name" : "A",
+ "value" : "1",
+ "description" : ""
+ } ]
+ }
+ """;
+ Files.writeString(path, original);
+
+
assertFalse(EnvironmentConfigFileSummary.saveDescriptionIfChanged(path.toString(),
" keep "));
+ assertEquals(original, Files.readString(path));
+ }
+
+ @Test
+ void changedDescriptionKeepsVariablesAndOtherKeys() throws Exception {
+ Path path = folder.resolve("change.json");
+ Files.writeString(
+ path,
+ """
+ { "description" : "old", "extra" : "keep", "variables" : [ { "name" :
"A", "value" : "1", "description" : "d" } ] }
+ """);
+
+
assertTrue(EnvironmentConfigFileSummary.saveDescriptionIfChanged(path.toString(),
" new "));
+
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(path.toString());
+ loaded.readFromFile();
+ assertEquals("new", loaded.getDescription());
+ assertEquals("keep", loaded.getConfigMap().get("extra"));
+ assertEquals("1", loaded.findDescribedVariableValue("A"));
+ assertEquals("d", loaded.findDescribedVariable("A").getDescription());
+ }
+
+ @Test
+ void clearingTheDescriptionRemovesTheKey() throws Exception {
+ Path path = folder.resolve("clear.json");
+ DescribedVariablesConfigFile created = new
DescribedVariablesConfigFile(path.toString());
+ created.setDescription("gone");
+ created.setDescribedVariable(new DescribedVariable("A", "1", ""));
+ created.saveToFile();
+
+
assertTrue(EnvironmentConfigFileSummary.saveDescriptionIfChanged(path.toString(),
" "));
+
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(path.toString());
+ loaded.readFromFile();
+ assertNull(loaded.getDescription());
+
assertNull(loaded.getConfigMap().get(DescribedVariablesConfigFile.HOP_DESCRIPTION_KEY));
+ assertEquals("1", loaded.findDescribedVariableValue("A"));
+ }
+
+ @Test
+ void createsAMissingFileOnlyWhenTheDescriptionIsSet() throws Exception {
+ Path created = folder.resolve("created.json");
+
assertFalse(EnvironmentConfigFileSummary.saveDescriptionIfChanged(created.toString(),
" "));
+ assertFalse(Files.exists(created));
+
+ assertTrue(
+
EnvironmentConfigFileSummary.saveDescriptionIfChanged(created.toString(),
"Created"));
+ DescribedVariablesConfigFile loaded = new
DescribedVariablesConfigFile(created.toString());
+ loaded.readFromFile();
+ assertEquals("Created", loaded.getDescription());
+ assertTrue(loaded.getDescribedVariables().isEmpty());
+ }
+
+ @Test
+ void refusesToOverwriteAnUnreadableFile() throws Exception {
+ Path broken = folder.resolve("broken.json");
+ Files.writeString(broken, "{");
+
+
assertFalse(EnvironmentConfigFileSummary.saveDescriptionIfChanged(broken.toString(),
""));
+ assertEquals("{", Files.readString(broken));
+
+ HopException error =
+ assertThrows(
+ HopException.class,
+ () ->
EnvironmentConfigFileSummary.saveDescriptionIfChanged(broken.toString(),
"new"));
+ assertTrue(error.getMessage().contains(broken.toString()));
+ assertEquals("{", Files.readString(broken));
+ }
+
+ private static void write(Path path, String json) throws Exception {
+ Files.writeString(path, json);
+ }
+
+ private static String message(String key) {
+ return BaseMessages.getString(EnvironmentConfigFileSummary.class, key);
+ }
+}
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/util/ProjectsConfigHelperTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/util/ProjectsConfigHelperTest.java
index 17d57a9861..3c798dd61f 100644
---
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/util/ProjectsConfigHelperTest.java
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/util/ProjectsConfigHelperTest.java
@@ -34,6 +34,7 @@ import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.hop.core.Const;
+import org.apache.hop.core.config.DescribedVariablesConfigFile;
import org.apache.hop.core.config.HopConfig;
import org.apache.hop.core.logging.HopLogStore;
import org.apache.hop.core.logging.LogChannel;
@@ -327,6 +328,12 @@ public class ProjectsConfigHelperTest {
variables.getVariable(ProjectsUtil.VARIABLE_PROJECT_HOME));
assertEquals("test_val", variables.getVariable("TEST_ENV_VAR"));
assertEquals("my-env",
variables.getVariable(Defaults.VARIABLE_HOP_ENVIRONMENT_NAME));
+
+ DescribedVariablesConfigFile environmentFile =
+ new DescribedVariablesConfigFile(confFile.toString());
+ environmentFile.readFromFile();
+ assertEquals("my environment config", environmentFile.getDescription());
+ assertEquals("test_val",
environmentFile.findDescribedVariableValue("TEST_ENV_VAR"));
}
@Test
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/dialog/HopDescribedVariablesDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/HopDescribedVariablesDialog.java
index b3eafc3fb2..e7904ce413 100644
---
a/ui/src/main/java/org/apache/hop/ui/core/dialog/HopDescribedVariablesDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/core/dialog/HopDescribedVariablesDialog.java
@@ -37,10 +37,12 @@ import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableItem;
+import org.eclipse.swt.widgets.Text;
/** Allows the user to edit the system settings of the hop.config file. */
public class HopDescribedVariablesDialog extends Dialog {
@@ -54,6 +56,14 @@ public class HopDescribedVariablesDialog extends Dialog {
private List<DescribedVariable> describedVariables;
private String selectedVariable;
+ /**
+ * When true, the dialog edits the configuration file's own description as
well as its variables.
+ */
+ private boolean editFileDescription;
+
+ private String fileDescription;
+ private Text wFileDescription;
+
/**
* Constructs a new dialog
*
@@ -72,6 +82,29 @@ public class HopDescribedVariablesDialog extends Dialog {
props = PropsUi.getInstance();
}
+ /**
+ * Show a single-line description stored on the configuration file itself,
next to its variables.
+ *
+ * <p>Call this before {@link #open()}. System variables, database variables
and the import dialog
+ * leave it off: only an environment configuration file has this field.
+ *
+ * @param description current file description, or null when the file has
none
+ */
+ public void setFileDescriptionEditing(String description) {
+ this.editFileDescription = true;
+ this.fileDescription = description;
+ }
+
+ /**
+ * The file description from the dialog.
+ *
+ * @return the entered text, or null when the dialog was cancelled or file
descriptions are not
+ * being edited
+ */
+ public String getFileDescription() {
+ return fileDescription;
+ }
+
public List<DescribedVariable> open() {
Shell parent = getParent();
@@ -117,6 +150,31 @@ public class HopDescribedVariablesDialog extends Dialog {
fdlFields.top = new FormAttachment(0, margin);
wlFields.setLayoutData(fdlFields);
+ Control descriptionAnchor = wlFields;
+ if (editFileDescription) {
+ Label wlFileDescription = new Label(shell, SWT.NONE);
+ wlFileDescription.setText(
+ BaseMessages.getString(PKG,
"HopDescribedVariablesDialog.FileDescription.Label"));
+ PropsUi.setLook(wlFileDescription);
+ FormData fdlFileDescription = new FormData();
+ fdlFileDescription.left = new FormAttachment(0, 0);
+ fdlFileDescription.top = new FormAttachment(wlFields, margin);
+ fdlFileDescription.right = new FormAttachment(100, 0);
+ wlFileDescription.setLayoutData(fdlFileDescription);
+
+ wFileDescription = new Text(shell, SWT.BORDER | SWT.SINGLE);
+ PropsUi.setLook(wFileDescription);
+ wFileDescription.setText(Const.NVL(fileDescription, ""));
+ wFileDescription.setToolTipText(
+ BaseMessages.getString(PKG,
"HopDescribedVariablesDialog.FileDescription.ToolTip"));
+ FormData fdFileDescription = new FormData();
+ fdFileDescription.left = new FormAttachment(0, 0);
+ fdFileDescription.top = new FormAttachment(wlFileDescription, margin);
+ fdFileDescription.right = new FormAttachment(100, 0);
+ wFileDescription.setLayoutData(fdFileDescription);
+ descriptionAnchor = wFileDescription;
+ }
+
int fieldsRows = 0;
ColumnInfo[] columns = {
@@ -155,7 +213,7 @@ public class HopDescribedVariablesDialog extends Dialog {
FormData fdFields = new FormData();
fdFields.left = new FormAttachment(0, 0);
- fdFields.top = new FormAttachment(wlFields, 2 * margin);
+ fdFields.top = new FormAttachment(descriptionAnchor, 2 * margin);
fdFields.right = new FormAttachment(100, 0);
fdFields.bottom = new FormAttachment(wOk, -2 * margin);
wFields.setLayoutData(fdFields);
@@ -209,10 +267,14 @@ public class HopDescribedVariablesDialog extends Dialog {
private void cancel() {
describedVariables = null;
+ fileDescription = null;
dispose();
}
private void ok() {
+ if (editFileDescription && wFileDescription != null &&
!wFileDescription.isDisposed()) {
+ fileDescription = wFileDescription.getText();
+ }
describedVariables.clear();
for (int i = 0; i < wFields.nrNonEmpty(); i++) {
TableItem item = wFields.getNonEmpty(i);
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index d65c6b167e..79b2a51123 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -3103,12 +3103,17 @@ public class HopGui
String selectedVariable)
throws HopException {
String message = "Editing configuration file: " + configFilename;
+ // Read this before getDescribedVariables(): that call can replace the
config map with a nested
+ // "config" object, which would hide a description stored next to the
variables.
+ String fileDescription = variablesConfigFile.getDescription();
HopDescribedVariablesDialog variablesDialog =
new HopDescribedVariablesDialog(
shell, message, variablesConfigFile.getDescribedVariables(),
selectedVariable);
+ variablesDialog.setFileDescriptionEditing(fileDescription);
List<DescribedVariable> vars = variablesDialog.open();
if (vars != null) {
variablesConfigFile.setDescribedVariables(vars);
+ variablesConfigFile.setDescription(variablesDialog.getFileDescription());
variablesConfigFile.saveToFile();
return true;
}
diff --git
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
index cf8ecde5a4..9500563d78 100644
---
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
@@ -319,6 +319,8 @@ GetQueryFieldsProgressDialog.Error.Title=Error getting
information
GetTableSizeProgressDialog.Error.Message=An error occured getting information
from the database\!
GetTableSizeProgressDialog.Error.Title=Error getting information
HopDescribedVariablesDialog.Button.EncodeValue=Encode value
+HopDescribedVariablesDialog.FileDescription.Label=Description
+HopDescribedVariablesDialog.FileDescription.ToolTip=Short note stored in this
configuration file, next to its variables
HopDescribedVariablesDialog.Title=Hop described variables dialog
HopPropertiesFileDialog.Description.Label=Description
HopPropertiesFileDialog.Exception.ErrorLoadingData.Message=Error loading data