This is an automated email from the ASF dual-hosted git repository.
mattcasters 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 e6b93f7d73 Fixes #8124 : Populate the info fields of pipelines and
workflows (#8631)
e6b93f7d73 is described below
commit e6b93f7d730122901f208741f87743b42b1d2333
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Oct 1 20:41:22 2026 +0200
Fixes #8124 : Populate the info fields of pipelines and workflows (#8631)
* Fixes #8124 : Populate the info fields of pipelines and workflows
* Issue #8124 : Address review comments
- Do not record the privilege-mode stand-in user in audit fields
- Keep pipeline and workflow version fields, used by the git diff markers
---
.../org/apache/hop/core/security/HopSecurity.java | 23 ++
.../core/security/HopSecurityPrivilegeMode.java | 8 +-
.../ROOT/pages/pipeline/create-pipeline.adoc | 10 +-
.../pages/pipeline/transforms/getsystemdata.adoc | 8 +-
.../ROOT/pages/workflow/create-workflow.adoc | 9 +-
.../java/org/apache/hop/base/AbstractMeta.java | 47 ++++
.../org/apache/hop/pipeline/PipelineMetaInfo.java | 10 +-
.../org/apache/hop/base/ModificationStampTest.java | 273 +++++++++++++++++++++
.../pipeline/transforms/systemdata/SystemData.java | 4 +
.../transforms/systemdata/SystemDataMeta.java | 2 +
.../transforms/systemdata/SystemDataType.java | 2 +
.../systemdata/messages/messages_en_US.properties | 2 +
.../hopgui/file/pipeline/HopGuiPipelineGraph.java | 7 +-
.../hopgui/file/pipeline/HopPipelineFileType.java | 3 +-
.../hopgui/file/workflow/HopGuiWorkflowGraph.java | 7 +-
.../hopgui/file/workflow/HopWorkflowFileType.java | 3 +-
16 files changed, 400 insertions(+), 18 deletions(-)
diff --git a/core/src/main/java/org/apache/hop/core/security/HopSecurity.java
b/core/src/main/java/org/apache/hop/core/security/HopSecurity.java
index bdf4eadcdd..50e58855e2 100644
--- a/core/src/main/java/org/apache/hop/core/security/HopSecurity.java
+++ b/core/src/main/java/org/apache/hop/core/security/HopSecurity.java
@@ -17,6 +17,8 @@
package org.apache.hop.core.security;
+import java.util.Optional;
+
/**
* Global entry point for authorization checks. Defaults to an unrestricted
context (desktop / no
* auth). Hop Web installs a session-aware {@link ISecurityContextProvider} so
concurrent users each
@@ -70,6 +72,27 @@ public final class HopSecurity {
return context != null ? context : HopSecurityContext.unrestricted();
}
+ /**
+ * The name to record in the audit fields of a file, such as the user who
created or last modified
+ * a pipeline or workflow.
+ *
+ * <p>Only an authenticated context yields a name. The desktop runs
unrestricted under the {@link
+ * HopSecurityContext#ANONYMOUS_USERNAME} placeholder, and neither that
placeholder nor the
+ * operating system account name says anything about who edited the file, so
those cases return an
+ * empty result and leave whatever the file already holds untouched. The
same goes for the {@link
+ * HopSecurityPrivilegeMode#STAND_IN_USERNAME} stand-in used when the
desktop simulates a role.
+ *
+ * @return the authenticated user name, or empty when no real user is known
+ */
+ public static Optional<String> getAuditUsername() {
+ HopSecurityContext context = getContext();
+ if (!context.isAuthenticated()
+ ||
HopSecurityPrivilegeMode.STAND_IN_USERNAME.equals(context.getUsername())) {
+ return Optional.empty();
+ }
+ return Optional.of(context.getUsername());
+ }
+
/**
* @param permission permission to check
* @return true if the current context allows it
diff --git
a/core/src/main/java/org/apache/hop/core/security/HopSecurityPrivilegeMode.java
b/core/src/main/java/org/apache/hop/core/security/HopSecurityPrivilegeMode.java
index 83f50d1aa7..5fb3674401 100644
---
a/core/src/main/java/org/apache/hop/core/security/HopSecurityPrivilegeMode.java
+++
b/core/src/main/java/org/apache/hop/core/security/HopSecurityPrivilegeMode.java
@@ -32,6 +32,12 @@ public final class HopSecurityPrivilegeMode {
/** Sentinel for “use full base privileges” (not a HopRole id). */
public static final String MODE_FULL = "full";
+ /**
+ * User name given to an effective context when the base context has no real
user, such as the
+ * unrestricted desktop simulating a role.
+ */
+ public static final String STAND_IN_USERNAME = "session";
+
private HopSecurityPrivilegeMode() {}
/**
@@ -86,7 +92,7 @@ public final class HopSecurityPrivilegeMode {
if (username == null
|| username.isBlank()
|| HopSecurityContext.ANONYMOUS_USERNAME.equals(username)) {
- username = "session";
+ username = STAND_IN_USERNAME;
}
return HopSecurityContext.forUser(username, EnumSet.of(target),
base.getSourceRoles());
}
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
index afda546891..acbf7d313b 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/create-pipeline.adoc
@@ -181,12 +181,14 @@ The Pipeline tab allows you to specify general properties
about the pipeline inc
|Pipeline filename| The filename of the pipeline
|Description|Short description of the pipeline
|Extended description| Long extended description of the pipeline
-|Status| Draft or production status
+|Status| Draft or production status. Deprecated: nothing in Hop acts on this
value and it will be removed in a future release.
|Version|Description of the version
-|Created by| Displays the original creator of the pipeline
+|Created by| The user that created the pipeline. Only recorded when Hop knows
who that is, such as an authenticated Hop web session, and shows `-` otherwise.
|Created at|Displays the date and time when the pipeline was created.
-|Last modified by| Displays the last user that modified the pipeline
-|Last modified at|Displays the date and time when the pipeline was last
modified.
+|Last modified by| The user that last modified the pipeline, recorded under
the same conditions as `Created by`.
+|Last modified at|Displays the date and time of the last save that changed the
pipeline. A save that does not change anything leaves this untouched.
+|Created with|The version of Hop that created the pipeline. Empty for
pipelines created before Hop started recording this.
+|Last modified with|The version of Hop that last saved a change to the
pipeline.
|===
The parameters tab allows you to specify parameters specific for the pipeline.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc
index 7adf9ba6b9..e4a6a88548 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc
@@ -93,9 +93,13 @@ See also Launching several copies of a transform. | `copy of
transform`
|Pipeline file name|File name of the pipeline (XML only). | This code has
leading white space : `{sp}pipeline file name`
-|User that modified the pipeline last| | `User modified`
+|User that created the pipeline|Only filled in when Hop knew who created the
pipeline, such as an authenticated Hop web session. `-` otherwise. | `User
created`
-|Date when the pipeline was modified last| | `Date modified`
+|Date when the pipeline was created|The date the pipeline was first created. |
`Date created`
+
+|User that modified the pipeline last|Only filled in when Hop knew who saved
the pipeline last, such as an authenticated Hop web session. `-` otherwise. |
`User modified`
+
+|Date when the pipeline was modified last|The date of the last save that
changed the pipeline. | `Date modified`
|Hostname (Network setup)|Returns the hostname of the server. | `Hostname`
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
b/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
index 3b9aef41fd..51ff58bd21 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/create-workflow.adoc
@@ -144,12 +144,13 @@ The Workflow tab allows you to specify general properties
about the workflow inc
|Workflow filename| The filename of the workflow
|Description|Short description of the workflow
|Extended description| Long extended description of the workflow
-|Status| Draft or production status
|Version|Description of the version
-|Created by| Displays the original creator of the workflow
+|Created by| The user that created the workflow. Only recorded when Hop knows
who that is, such as an authenticated Hop web session, and shows `-` otherwise.
|Created at|Displays the date and time when the workflow was created.
-|Last modified by| Displays the last user that modified the workflow
-|Last modified at|Displays the date and time when the workflow was last
modified.
+|Last modified by| The user that last modified the workflow, recorded under
the same conditions as `Created by`.
+|Last modified at|Displays the date and time of the last save that changed the
workflow. A save that does not change anything leaves this untouched.
+|Created with|The version of Hop that created the workflow. Empty for
workflows created before Hop started recording this.
+|Last modified with|The version of Hop that last saved a change to the
workflow.
|===
The parameters tab allows you to specify parameters specific for the workflow.
diff --git a/engine/src/main/java/org/apache/hop/base/AbstractMeta.java
b/engine/src/main/java/org/apache/hop/base/AbstractMeta.java
index 46da7d0153..3cb1238f95 100644
--- a/engine/src/main/java/org/apache/hop/base/AbstractMeta.java
+++ b/engine/src/main/java/org/apache/hop/base/AbstractMeta.java
@@ -20,6 +20,7 @@ package org.apache.hop.base;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -49,6 +50,7 @@ import org.apache.hop.core.parameters.DuplicateParamException;
import org.apache.hop.core.parameters.INamedParameterDefinitions;
import org.apache.hop.core.parameters.INamedParameters;
import org.apache.hop.core.parameters.UnknownParamException;
+import org.apache.hop.core.security.HopSecurity;
import org.apache.hop.core.undo.ChangeAction;
import org.apache.hop.core.util.Utils;
import org.apache.hop.core.variables.IVariables;
@@ -1001,6 +1003,51 @@ public abstract class AbstractMeta
}
}
+ /**
+ * Whether a save that is about to be written has to refresh the elements
recording who last
+ * changed this file and when.
+ *
+ * <p>An edited file gets a fresh stamp, and so does a file that is being
written for the first
+ * time. A save that writes the same content over a file that is already
there does not: the
+ * elements would then change every time an untouched file is saved and show
up as a difference in
+ * version control.
+ *
+ * @param fileExists whether the file about to be written is already there
+ * @return true if the file has to be stamped before it is written
+ */
+ public boolean needsModificationStamp(boolean fileExists) {
+ return hasChanged() || !fileExists;
+ }
+
+ /**
+ * Record the current time and user as the last modification of this file.
Called by the save path
+ * right before the file is written, when {@link
#needsModificationStamp(boolean)} says so.
+ *
+ * <p>The user is only recorded when a real one is known, see {@link
+ * HopSecurity#getAuditUsername()}. The desktop has no authenticated user,
so there the modified
+ * date is maintained and the user is left at whatever the file already
holds.
+ */
+ public void stampModified() {
+ setModifiedDate(new Date());
+ HopSecurity.getAuditUsername().ifPresent(this::setModifiedUser);
+ }
+
+ /**
+ * Record the current user as the creator of this file. Called when a new
file is created, never
+ * on save: the creator of an existing file is not whoever happens to save
it next.
+ *
+ * <p>The creation date is set by the constructor. Only a known user is
recorded, see {@link
+ * HopSecurity#getAuditUsername()}.
+ */
+ public void stampCreated() {
+ HopSecurity.getAuditUsername()
+ .ifPresent(
+ username -> {
+ setCreatedUser(username);
+ setModifiedUser(username);
+ });
+ }
+
public void addObserver(IHopObserver o) {
changedFlag.addObserver(o);
}
diff --git a/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaInfo.java
b/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaInfo.java
index 58db96e4f4..5c86cff219 100644
--- a/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaInfo.java
+++ b/engine/src/main/java/org/apache/hop/pipeline/PipelineMetaInfo.java
@@ -42,7 +42,15 @@ public class PipelineMetaInfo extends AbstractMetaInfo {
@HopMetadataProperty(key = "transform_performance_capturing_size_limit")
protected String transformPerformanceCapturingSizeLimit;
- /** The status of the pipeline. */
+ /**
+ * The status of the pipeline: {@code 1} for draft, {@code 2} for
production, {@code -1} when
+ * unset.
+ *
+ * @deprecated since 2.20, for removal. Nothing in Hop acts on the value, it
has to be maintained
+ * by hand, and it is left at {@code -1} in practice. It is still read
from and written to the
+ * file, so no existing value is lost before it is removed.
+ */
+ @Deprecated(since = "2.20", forRemoval = true)
@HopMetadataProperty(key = "pipeline_status")
protected int pipelineStatus;
diff --git
a/engine/src/test/java/org/apache/hop/base/ModificationStampTest.java
b/engine/src/test/java/org/apache/hop/base/ModificationStampTest.java
new file mode 100644
index 0000000000..572ef4189f
--- /dev/null
+++ b/engine/src/test/java/org/apache/hop/base/ModificationStampTest.java
@@ -0,0 +1,273 @@
+/*
+ * 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.base;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.Set;
+import org.apache.hop.core.security.HopRole;
+import org.apache.hop.core.security.HopSecurity;
+import org.apache.hop.core.security.HopSecurityContext;
+import org.apache.hop.core.security.HopSecurityPrivilegeMode;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.core.xml.XmlHandler;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.workflow.WorkflowMeta;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * The creation and last modification of a pipeline or workflow are recorded
in the file so a
+ * project can be reviewed for stale or unattended work.
+ *
+ * <p>Both are only recorded when there is something to record: the date of
the last change, and the
+ * user behind it whenever the session knows one. A desktop session has no
authenticated user, and
+ * neither that fact nor the operating system account says who edited the
file, so the user elements
+ * keep whatever the file already held rather than being filled with a
placeholder.
+ */
+@ExtendWith(RestoreHopEngineEnvironmentExtension.class)
+class ModificationStampTest {
+
+ /** The placeholder written by every Hop release so far when no user is
known. */
+ private static final String NO_USER = "-";
+
+ private static final String AUTHOR = "alice";
+ private static final String EDITOR = "bob";
+
+ /** A pipeline as written by an older release: dates filled in, users left
at the placeholder. */
+ private static final String PIPELINE_WITHOUT_USERS =
+ """
+ <pipeline>
+ <info>
+ <name>no-users</name>
+ <created_user>-</created_user>
+ <created_date>2023/09/16 22:31:19.820</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2023/09/16 22:31:19.820</modified_date>
+ </info>
+ </pipeline>
+ """;
+
+ /** A workflow as written by an older release, keeping its info elements at
the root. */
+ private static final String WORKFLOW_WITHOUT_USERS =
+ """
+ <workflow>
+ <name>no-users</name>
+ <created_user>-</created_user>
+ <created_date>2023/09/16 22:31:19.820</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2023/09/16 22:31:19.820</modified_date>
+ </workflow>
+ """;
+
+ private final IVariables variables = new Variables();
+ private final IHopMetadataProvider metadataProvider = new
MemoryMetadataProvider();
+
+ @AfterEach
+ void restoreSecurityContext() {
+ HopSecurity.reset();
+ }
+
+ private void authenticateAs(String username) {
+ HopSecurity.setProvider(() -> HopSecurityContext.forUser(username,
Set.of(HopRole.USER)));
+ }
+
+ private PipelineMeta loadPipeline(String xml) throws Exception {
+ return new PipelineMeta(
+ new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
+ metadataProvider,
+ variables);
+ }
+
+ private WorkflowMeta loadWorkflow(String xml) throws Exception {
+ return new WorkflowMeta(
+ new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)),
+ metadataProvider,
+ variables);
+ }
+
+ @Test
+ void pipelineModificationDateIsStampedAndSerialized() throws Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+ Date loadedDate = pipelineMeta.getModifiedDate();
+
+ pipelineMeta.stampModified();
+
+ assertTrue(
+ pipelineMeta.getModifiedDate().after(loadedDate),
+ "the modification date has to move forward when the file is saved
after an edit");
+ String stamped = XmlHandler.date2string(pipelineMeta.getModifiedDate());
+ assertTrue(
+ pipelineMeta.getXml(variables).contains("<modified_date>" + stamped +
"</modified_date>"),
+ "the stamped date has to end up in the file");
+ }
+
+ @Test
+ void workflowModificationDateIsStamped() throws Exception {
+ WorkflowMeta workflowMeta = loadWorkflow(WORKFLOW_WITHOUT_USERS);
+ Date loadedDate = workflowMeta.getModifiedDate();
+
+ workflowMeta.stampModified();
+
+ assertTrue(
+ workflowMeta.getModifiedDate().after(loadedDate),
+ "the modification date has to move forward when the file is saved
after an edit");
+ }
+
+ @Test
+ void authenticatedUserIsRecordedAsTheLastToModify() throws Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+ authenticateAs(EDITOR);
+
+ pipelineMeta.stampModified();
+
+ assertEquals(EDITOR, pipelineMeta.getModifiedUser());
+ assertTrue(
+ pipelineMeta.getXml(variables).contains("<modified_user>" + EDITOR +
"</modified_user>"));
+ }
+
+ @Test
+ void aModificationNeverRewritesTheCreator() throws Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+ Date createdDate = pipelineMeta.getCreatedDate();
+ authenticateAs(EDITOR);
+
+ pipelineMeta.stampModified();
+
+ assertEquals(
+ NO_USER,
+ pipelineMeta.getCreatedUser(),
+ "whoever saves a file is not its creator: an unknown creator stays
unknown");
+ assertEquals(createdDate, pipelineMeta.getCreatedDate());
+ }
+
+ @Test
+ void aKnownCreatorSurvivesAModificationBySomeoneElse() throws Exception {
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ authenticateAs(AUTHOR);
+ pipelineMeta.stampCreated();
+
+ authenticateAs(EDITOR);
+ pipelineMeta.stampModified();
+
+ assertEquals(AUTHOR, pipelineMeta.getCreatedUser());
+ assertEquals(EDITOR, pipelineMeta.getModifiedUser());
+ }
+
+ @Test
+ void creationRecordsTheAuthenticatedUserAsCreatorAndModifier() {
+ authenticateAs(AUTHOR);
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ WorkflowMeta workflowMeta = new WorkflowMeta();
+
+ pipelineMeta.stampCreated();
+ workflowMeta.stampCreated();
+
+ assertEquals(AUTHOR, pipelineMeta.getCreatedUser());
+ assertEquals(AUTHOR, pipelineMeta.getModifiedUser());
+ assertEquals(AUTHOR, workflowMeta.getCreatedUser());
+ assertEquals(AUTHOR, workflowMeta.getModifiedUser());
+ }
+
+ @Test
+ void withoutAnAuthenticatedUserTheUserElementsAreLeftAlone() throws
Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+ WorkflowMeta workflowMeta = loadWorkflow(WORKFLOW_WITHOUT_USERS);
+
+ // The desktop default: an unrestricted context with an anonymous
placeholder for a name.
+ pipelineMeta.stampCreated();
+ pipelineMeta.stampModified();
+ workflowMeta.stampCreated();
+ workflowMeta.stampModified();
+
+ assertEquals(NO_USER, pipelineMeta.getCreatedUser());
+ assertEquals(NO_USER, pipelineMeta.getModifiedUser());
+ assertEquals(NO_USER, workflowMeta.getCreatedUser());
+ assertEquals(NO_USER, workflowMeta.getModifiedUser());
+ assertTrue(
+
pipelineMeta.getXml(variables).contains("<modified_user>-</modified_user>"),
+ "a file saved on the desktop keeps the placeholder earlier releases
wrote");
+ }
+
+ @Test
+ void anUntouchedFileThatIsSavedAgainIsNotStamped() throws Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+
+ assertFalse(
+ pipelineMeta.needsModificationStamp(true),
+ "saving an untouched file over the existing one has to leave the file
as it is");
+
+ pipelineMeta.setChanged();
+
+ assertTrue(
+ pipelineMeta.needsModificationStamp(true),
+ "saving an edited file has to record the change");
+ }
+
+ @Test
+ void aFileWrittenForTheFirstTimeIsAlwaysStamped() throws Exception {
+ PipelineMeta pipelineMeta = loadPipeline(PIPELINE_WITHOUT_USERS);
+
+ assertTrue(
+ pipelineMeta.needsModificationStamp(false),
+ "a file that is not there yet is being written, however the changed
flag stands");
+ }
+
+ @Test
+ void theAnonymousPlaceholderIsNeverWrittenAsAUser() {
+ HopSecurity.setProvider(HopSecurityContext::unrestricted);
+ PipelineMeta pipelineMeta = new PipelineMeta();
+
+ pipelineMeta.stampCreated();
+ pipelineMeta.stampModified();
+
+ assertEquals(NO_USER, pipelineMeta.getCreatedUser());
+ assertEquals(NO_USER, pipelineMeta.getModifiedUser());
+ }
+
+ @Test
+ void aDesktopRoleSimulationIsNeverWrittenAsAUser() {
+ HopSecurity.setProvider(
+ () ->
+ HopSecurityPrivilegeMode.createEffective(
+ HopSecurityContext.unrestricted(), HopRole.READ_ONLY));
+ PipelineMeta pipelineMeta = new PipelineMeta();
+ WorkflowMeta workflowMeta = new WorkflowMeta();
+
+ pipelineMeta.stampCreated();
+ pipelineMeta.stampModified();
+ workflowMeta.stampCreated();
+ workflowMeta.stampModified();
+
+ assertEquals(NO_USER, pipelineMeta.getCreatedUser());
+ assertEquals(NO_USER, pipelineMeta.getModifiedUser());
+ assertEquals(NO_USER, workflowMeta.getCreatedUser());
+ assertEquals(NO_USER, workflowMeta.getModifiedUser());
+ }
+}
diff --git
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemData.java
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemData.java
index 7085699075..23165f6e67 100644
---
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemData.java
+++
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemData.java
@@ -20,6 +20,8 @@ package org.apache.hop.pipeline.transforms.systemdata;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.AVAILABLE_PROCESSORS;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.COMMITTED_VIRTUAL_MEMORY_SIZE;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.COPYNR;
+import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.CREATED_DATE;
+import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.CREATED_USER;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.CURRENT_PID;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.FILENAME;
import static
org.apache.hop.pipeline.transforms.systemdata.SystemDataType.FREE_PHYSICAL_MEMORY_SIZE;
@@ -311,6 +313,8 @@ public class SystemData extends
BaseTransform<SystemDataMeta, SystemDataData> {
resolvers.put(COPYNR, () -> (long) getCopy());
resolvers.put(PIPELINE_NAME, () -> getPipelineMeta().getName());
resolvers.put(FILENAME, () -> getPipelineMeta().getFilename());
+ resolvers.put(CREATED_USER, () -> getPipelineMeta().getCreatedUser());
+ resolvers.put(CREATED_DATE, () -> getPipelineMeta().getCreatedDate());
resolvers.put(MODIFIED_USER, () -> getPipelineMeta().getModifiedUser());
resolvers.put(MODIFIED_DATE, () -> getPipelineMeta().getModifiedDate());
resolvers.put(HOSTNAME_REAL, Const::getHostnameReal);
diff --git
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataMeta.java
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataMeta.java
index 3cd9bebba8..e63a7c7e33 100644
---
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataMeta.java
+++
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataMeta.java
@@ -108,6 +108,7 @@ public class SystemDataMeta extends
BaseTransformMeta<SystemData, SystemDataData
THIS_MONTH_END,
NEXT_MONTH_START,
NEXT_MONTH_END,
+ CREATED_DATE,
MODIFIED_DATE,
PREV_WEEK_START,
PREV_WEEK_END,
@@ -140,6 +141,7 @@ public class SystemDataMeta extends
BaseTransformMeta<SystemData, SystemDataData
break;
case PIPELINE_NAME,
FILENAME,
+ CREATED_USER,
MODIFIED_USER,
HOSTNAME,
HOSTNAME_REAL,
diff --git
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataType.java
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataType.java
index 825f229d68..6aab842cef 100644
---
a/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataType.java
+++
b/plugins/transforms/systemdata/src/main/java/org/apache/hop/pipeline/transforms/systemdata/SystemDataType.java
@@ -46,6 +46,8 @@ public enum SystemDataType implements
IEnumHasCodeAndDescription {
COPYNR("copy of transform", "CopyOfTransform"),
PIPELINE_NAME(" pipeline name", "PipelineName"),
FILENAME(" pipeline file name", "PipelineFileName"),
+ CREATED_USER("User created", "UserCreated"),
+ CREATED_DATE("Date created", "DateCreated"),
MODIFIED_USER("User modified", "UserModified"),
MODIFIED_DATE("Date modified", "DateModified"),
HOSTNAME("Hostname", "HostnameNetworkSetup"),
diff --git
a/plugins/transforms/systemdata/src/main/resources/org/apache/hop/pipeline/transforms/systemdata/messages/messages_en_US.properties
b/plugins/transforms/systemdata/src/main/resources/org/apache/hop/pipeline/transforms/systemdata/messages/messages_en_US.properties
index 6c5955c186..d24be0468b 100644
---
a/plugins/transforms/systemdata/src/main/resources/org/apache/hop/pipeline/transforms/systemdata/messages/messages_en_US.properties
+++
b/plugins/transforms/systemdata/src/main/resources/org/apache/hop/pipeline/transforms/systemdata/messages/messages_en_US.properties
@@ -53,6 +53,7 @@ SystemDataMeta.TypeDesc.CommandLineArgument9=Command line
argument 9
SystemDataMeta.TypeDesc.CommittedVirtualMemorySize=Committed virtual size
(bytes)
SystemDataMeta.TypeDesc.CopyOfTransform=Copy of transform
SystemDataMeta.TypeDesc.CurrentPID=Current process identifier (PID)
+SystemDataMeta.TypeDesc.DateCreated=Date when the pipeline was created
SystemDataMeta.TypeDesc.DateModified=Date when the pipeline was modified last
SystemDataMeta.TypeDesc.EndDateRange=End date range (Pipeline)
SystemDataMeta.TypeDesc.FreePhysicalMemorySize=Free physical memory size
(bytes)
@@ -131,6 +132,7 @@ SystemDataMeta.TypeDesc.TomorrowEnd=Tomorrow 23\:59\:59
SystemDataMeta.TypeDesc.TomorrowStart=Tomorrow 00\:00\:00
SystemDataMeta.TypeDesc.TotalPhysicalMemorySize=Total physical memory size
(bytes)
SystemDataMeta.TypeDesc.TotalSwapSpaceSize=Total swap space size (bytes)
+SystemDataMeta.TypeDesc.UserCreated=User that created the pipeline
SystemDataMeta.TypeDesc.UserModified=User that modified the pipeline last
SystemDataMeta.TypeDesc.YesterdayEnd=Yesterday 23\:59\:59
SystemDataMeta.TypeDesc.YesterdayStart=Yesterday 00\:00\:00
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
index 43be6f229d..15da8ee697 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopGuiPipelineGraph.java
@@ -5734,9 +5734,12 @@ public class HopGuiPipelineGraph extends
HopGuiAbstractGraph
boolean fileExist = HopVfs.fileExists(pipelineMeta.getFilename());
- // Record the version of Hop saving this pipeline
+ // Record who saved this pipeline, when, and with which version of Hop
//
- pipelineMeta.setModifiedHopVersion(Const.NVL(Const.getHopVersion(), ""));
+ if (pipelineMeta.needsModificationStamp(fileExist)) {
+ pipelineMeta.stampModified();
+ pipelineMeta.setModifiedHopVersion(Const.NVL(Const.getHopVersion(),
""));
+ }
String xml = pipelineMeta.getXml(variables);
OutputStream out = HopVfs.getOutputStream(pipelineMeta.getFilename(),
false);
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
index 301c947c4e..5c9e32059d 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/HopPipelineFileType.java
@@ -169,8 +169,9 @@ public class HopPipelineFileType<T extends PipelineMeta>
extends HopFileTypeBase
PipelineMeta pipelineMeta = new PipelineMeta();
pipelineMeta.setName(BaseMessages.getString(PKG,
"HopPipelineFileType.New.Text"));
- // Record the version of Hop creating this pipeline
+ // Record who created this pipeline and with which version of Hop
//
+ pipelineMeta.stampCreated();
pipelineMeta.setCreatedHopVersion(Const.NVL(Const.getHopVersion(), ""));
// Pass the MetadataProvider for reference lookups
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
index a40115d6b1..30e870f97b 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopGuiWorkflowGraph.java
@@ -4876,9 +4876,12 @@ public class HopGuiWorkflowGraph extends
HopGuiAbstractGraph
boolean fileExist = HopVfs.fileExists(workflowMeta.getFilename());
- // Record the version of Hop saving this workflow
+ // Record who saved this workflow, when, and with which version of Hop
//
- workflowMeta.setModifiedHopVersion(Const.NVL(Const.getHopVersion(), ""));
+ if (workflowMeta.needsModificationStamp(fileExist)) {
+ workflowMeta.stampModified();
+ workflowMeta.setModifiedHopVersion(Const.NVL(Const.getHopVersion(),
""));
+ }
String xml = workflowMeta.getXml(variables);
OutputStream out = HopVfs.getOutputStream(workflowMeta.getFilename(),
false);
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
index 16e29de2a8..8f957826b2 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/HopWorkflowFileType.java
@@ -174,8 +174,9 @@ public class HopWorkflowFileType<T extends WorkflowMeta>
extends HopFileTypeBase
WorkflowMeta workflowMeta = new WorkflowMeta();
workflowMeta.setName(BaseMessages.getString(PKG,
"HopWorkflowFileType.New.Text"));
- // Record the version of Hop creating this workflow
+ // Record who created this workflow and with which version of Hop
//
+ workflowMeta.stampCreated();
workflowMeta.setCreatedHopVersion(Const.NVL(Const.getHopVersion(), ""));
// Pass the MetaStore for reference lookups