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 bdf4eadcddb..50e58855e25 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 static HopSecurityContext getContext() { 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. + * + *

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 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 83f50d1aa7b..5fb36744015 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 static HopSecurityContext createEffective(HopSecurityContext base, HopRol 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 b52c5bdf3e2..fec5d14cf97 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 @@ -177,12 +177,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 7adf9ba6b91..e4a6a885483 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 3b9aef41fd5..51ff58bd21a 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 ef59cc1030b..0bf7d5f1855 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 @@ 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.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; @@ -989,6 +991,51 @@ public final void setChanged(boolean ch) { } } + /** + * Whether a save that is about to be written has to refresh the elements recording who last + * changed this file and when. + * + *

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. + * + *

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. + * + *

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 58db96e4f4a..5c86cff219d 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 00000000000..572ef4189fb --- /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. + * + *

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 = + """ + + + no-users + - + 2023/09/16 22:31:19.820 + - + 2023/09/16 22:31:19.820 + + + """; + + /** A workflow as written by an older release, keeping its info elements at the root. */ + private static final String WORKFLOW_WITHOUT_USERS = + """ + + no-users + - + 2023/09/16 22:31:19.820 + - + 2023/09/16 22:31:19.820 + + """; + + 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("" + stamped + ""), + "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("" + EDITOR + "")); + } + + @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("-"), + "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 7085699075b..23165f6e673 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 @@ 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 @@ private void initResolvers() { 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 3cd9bebba8f..e63a7c7e338 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 void getFields( THIS_MONTH_END, NEXT_MONTH_START, NEXT_MONTH_END, + CREATED_DATE, MODIFIED_DATE, PREV_WEEK_START, PREV_WEEK_END, @@ -140,6 +141,7 @@ public void getFields( 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 825f229d682..6aab842cefb 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 6c5955c186e..d24be0468bf 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 bd4b10340f7..b99c0ce12b7 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 @@ -5727,9 +5727,12 @@ public synchronized void save() throws HopException { 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 301c947c4e3..5c9e32059d8 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 IHopFileTypeHandler newFile(HopGui hopGui, IVariables variables) throws H 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 9afaddbec58..640d78d2880 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 @@ -4857,9 +4857,12 @@ public synchronized void save() throws HopException { 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 16e29de2a88..8f957826b27 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 IHopFileTypeHandler newFile(HopGui hopGui, IVariables parentVariableSpace 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