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

Reply via email to