This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new a9ad926e2b Issue #3127 : Copy parent project folders into child
projects (#8052)
a9ad926e2b is described below
commit a9ad926e2b34b4a8bc0fb7e9b9d2a8afc6fcee3a
Author: Matt Casters <[email protected]>
AuthorDate: Fri Aug 21 18:31:10 2026 +0200
Issue #3127 : Copy parent project folders into child projects (#8052)
---
.../hop-gui/environment/add-project-menu.png | Bin 0 -> 31111 bytes
.../environment/create-environment-dialogue.png | Bin 111443 -> 0 bytes
.../create-project-dialog-folders-tab.png | Bin 0 -> 79414 bytes
.../create-project-dialog-parent-project-tab.png | Bin 0 -> 72850 bytes
.../create-project-dialog-variables-tab.png | Bin 0 -> 58222 bytes
.../hop-gui/environment/create-project-dialog.png | Bin 115029 -> 100182 bytes
.../hop-gui/environment/edit-environment-menu.png | Bin 40294 -> 46669 bytes
.../images/hop-gui/environment/environment-add.png | Bin 0 -> 28732 bytes
.../images/hop-gui/environment/environment-add.svg | 49 --
.../hop-gui/environment/environment-delete.svg | 48 --
.../environment-dialog-configuration-files-tab.png | Bin 0 -> 70998 bytes
.../environment/environment-dialog-general-tab.png | Bin 0 -> 53607 bytes
.../environment-dialog-marketplace-plugins-tab.png | Bin 0 -> 85644 bytes
.../environment-dialog-system-resources-tab.png | Bin 0 -> 114784 bytes
.../hop-gui/environment/environment-edit.svg | 69 ---
.../hop-gui/environment/environment-files.png | Bin 27654 -> 45693 bytes
.../hop-gui/environment/environment-variables.png | Bin 106873 -> 151677 bytes
.../hop-gui/environment/new-environment-dialog.png | Bin 21553 -> 0 bytes
.../images/hop-gui/environment/new-environment.png | Bin 106890 -> 28732 bytes
.../images/hop-gui/environment/project-add.svg | 50 --
.../images/hop-gui/environment/project-delete.svg | 49 --
.../images/hop-gui/environment/project-edit.svg | 72 ---
.../environment/switch-environment-list.png | Bin 19746 -> 45219 bytes
.../hop-gui/environment/switch-environment.png | Bin 16515 -> 0 bytes
.../hop-gui/environment/switch-project-list.png | Bin 22703 -> 76080 bytes
.../the-active-project-at-the-bottom-left.png | Bin 0 -> 6156 bytes
.../modules/ROOT/pages/projects/advanced.adoc | 12 +
.../ROOT/pages/projects/projects-environments.adoc | 174 +++++--
.../environment/LifecycleEnvironmentDialog.java | 3 +
.../hop/projects/project/ParentProjectFolder.java | 51 ++
.../project/ParentProjectFolderSynchronizer.java | 442 ++++++++++++++++
.../org/apache/hop/projects/project/Project.java | 19 +
.../apache/hop/projects/project/ProjectDialog.java | 555 ++++++++++++---------
.../org/apache/hop/projects/util/Defaults.java | 3 +
.../org/apache/hop/projects/util/ProjectsUtil.java | 11 +
.../project/messages/messages_en_US.properties | 15 +
.../ParentProjectFolderSynchronizerTest.java | 282 +++++++++++
.../apache/hop/projects/project/ProjectTest.java | 31 ++
38 files changed, 1321 insertions(+), 614 deletions(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/add-project-menu.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/add-project-menu.png
new file mode 100644
index 0000000000..f3777cf98a
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/add-project-menu.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-environment-dialogue.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-environment-dialogue.png
deleted file mode 100644
index bca2a192a7..0000000000
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-environment-dialogue.png
and /dev/null differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-folders-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-folders-tab.png
new file mode 100644
index 0000000000..d43c6107f7
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-folders-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-parent-project-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-parent-project-tab.png
new file mode 100644
index 0000000000..0dea5468f8
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-parent-project-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-variables-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-variables-tab.png
new file mode 100644
index 0000000000..29fcb36208
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog-variables-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog.png
index 5ec612c763..1d565b2998 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/create-project-dialog.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/edit-environment-menu.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/edit-environment-menu.png
index 41982873a8..041b683653 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/edit-environment-menu.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/edit-environment-menu.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.png
new file mode 100644
index 0000000000..fe1487b407
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.svg
deleted file mode 100644
index 6df9a1c30b..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-add.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- version="1.1"
- id="Layer_1"
- x="0px"
- y="0px"
- width="42px"
- height="42px"
- viewBox="0 0 42 42"
- enable-background="new 0 0 42 42"
- xml:space="preserve"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- id="text816"
- y="19.834593"
- x="56.459057"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- xml:space="preserve"><tspan
- y="194.53062"
- x="56.459057"
- id="tspan814" /></text>
-
- <text
- id="text820"
- y="28.070263"
- x="2.978379"
-
style="font-style:normal;font-weight:normal;font-size:44.69970703px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.23281099"
- xml:space="preserve"><tspan
- style="stroke-width:0.23281099"
- y="28.070263"
- x="2.978379"
- id="tspan818">e</tspan></text>
-
- <g
- transform="matrix(0.48941052,0,0,0.48941052,24.528835,24.811866)"
- id="art"><polygon
- id="polygon6"
- points="18.2,0 13.8,0 13.8,13.8 0,13.8 0,18.2 13.8,18.2 13.8,32 18.2,32
18.2,18.2 32,18.2 32,13.8 18.2,13.8 "
- style="fill:#3d6380" /></g></svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-delete.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-delete.svg
deleted file mode 100644
index 2d54a855de..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-delete.svg
+++ /dev/null
@@ -1,48 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- version="1.1"
- id="Layer_1"
- x="0px"
- y="0px"
- width="42px"
- height="42px"
- viewBox="0 0 42 42"
- enable-background="new 0 0 42 42"
- xml:space="preserve"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- id="text816"
- y="19.834593"
- x="56.459057"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- xml:space="preserve"><tspan
- y="194.53062"
- x="56.459057"
- id="tspan814" /></text>
-
- <text
- id="text820"
- y="28.070263"
- x="2.978379"
-
style="font-style:normal;font-weight:normal;font-size:44.69970703px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.23281099"
- xml:space="preserve"><tspan
- style="stroke-width:0.23281099"
- y="28.070263"
- x="2.978379"
- id="tspan818">e</tspan></text>
-
- <polygon
- points="37.6,31.4 34.7,34.3 31.8,31.4 30.4,32.7 33.3,35.6
30.4,38.5 31.8,39.9 34.7,37 37.6,39.9 38.9,38.5 36,35.6 38.9,32.7 "
- id="polygon18"
- style="fill:#ea102a"
-
transform="matrix(1.6377839,0,0,1.6377839,-23.702184,-25.258686)"/></svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-configuration-files-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-configuration-files-tab.png
new file mode 100644
index 0000000000..a7db18fe08
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-configuration-files-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-general-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-general-tab.png
new file mode 100644
index 0000000000..7ac6b7774a
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-general-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-marketplace-plugins-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-marketplace-plugins-tab.png
new file mode 100644
index 0000000000..077aec3b3f
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-marketplace-plugins-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-system-resources-tab.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-system-resources-tab.png
new file mode 100644
index 0000000000..712e7af2b2
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-dialog-system-resources-tab.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-edit.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-edit.svg
deleted file mode 100644
index ee4eb98b0d..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-edit.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- version="1.1"
- id="Layer_1"
- x="0px"
- y="0px"
- width="42px"
- height="42px"
- viewBox="0 0 42 42"
- enable-background="new 0 0 42 42"
- xml:space="preserve"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- id="text816"
- y="19.834593"
- x="56.459057"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- xml:space="preserve"><tspan
- y="194.53062"
- x="56.459057"
- id="tspan814"></tspan></text>
- <text
- id="text820"
- y="28.070263"
- x="2.978379"
-
style="font-style:normal;font-weight:normal;font-size:44.69970703px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.23281099"
- xml:space="preserve"><tspan
- style="stroke-width:0.23281099"
- y="28.070263"
- x="2.978379"
- id="tspan818">e</tspan></text>
- <ellipse
- ry="2.0063434"
- rx="2.0330944"
- cy="34.986797"
- cx="25.54306"
- id="path2067"
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"/>
- <ellipse
- ry="0.22179212"
- rx="1.3307527"
- cy="35.715889"
- cx="29.202629"
- id="path2069"
- style="opacity:0.3;fill:#ffd700;fill-opacity:1;paint-order:markers
fill stroke"/>
- <ellipse
- ry="2.0063434"
- rx="2.0330944"
- cy="34.976582"
- cx="30.903036"
- id="path2067-1"
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"/>
- <ellipse
- ry="2.0063434"
- rx="2.0330944"
- cy="34.976585"
- cx="36.004257"
- id="path2067-6"
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"/></svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-files.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-files.png
index 7f7ed1f87c..9ba4d23a5f 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-files.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-files.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-variables.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-variables.png
index 6dfb097fde..39eed87919 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-variables.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/environment-variables.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment-dialog.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment-dialog.png
deleted file mode 100644
index 85a92af028..0000000000
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment-dialog.png
and /dev/null differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment.png
index 0cc1671f2b..fe1487b407 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/new-environment.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-add.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-add.svg
deleted file mode 100644
index fe03c0a1f5..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-add.svg
+++ /dev/null
@@ -1,50 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- xml:space="preserve"
- enable-background="new 0 0 42 42"
- viewBox="0 0 42 42"
- height="42px"
- width="42px"
- y="0px"
- x="0px"
- id="Layer_1"
- version="1.1"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- xml:space="preserve"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- x="56.459057"
- y="19.834593"
- id="text816"><tspan
- id="tspan814"
- x="56.459057"
- y="194.53062" /></text>
-
-
- <g
- id="art"
-
transform="matrix(0.48941052,0,0,0.48941052,24.528835,24.811866)"><polygon
- style="fill:#3d6380"
- points="18.2,0 13.8,0 13.8,13.8 0,13.8 0,18.2 13.8,18.2 13.8,32 18.2,32
18.2,18.2 32,18.2 32,13.8 18.2,13.8 "
- id="polygon6" /></g>
- <text
- id="text821"
- y="28.042698"
- x="2.8721724"
-
style="font-style:normal;font-weight:normal;font-size:48.81200027px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25422916"
- xml:space="preserve"><tspan
- style="stroke-width:0.25422916"
- y="28.042698"
- x="2.8721724"
- id="tspan819">p</tspan></text>
-</svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-delete.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-delete.svg
deleted file mode 100644
index 9f11d29cda..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-delete.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- xml:space="preserve"
- enable-background="new 0 0 42 42"
- viewBox="0 0 42 42"
- height="42px"
- width="42px"
- y="0px"
- x="0px"
- id="Layer_1"
- version="1.1"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- xml:space="preserve"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- x="56.459057"
- y="19.834593"
- id="text816"><tspan
- id="tspan814"
- x="56.459057"
- y="194.53062" /></text>
-
-
- <polygon
- transform="matrix(1.6377839,0,0,1.6377839,-23.702184,-25.258686)"
- style="fill:#ea102a"
- id="polygon18"
- points="37.6,31.4 34.7,34.3 31.8,31.4 30.4,32.7 33.3,35.6
30.4,38.5 31.8,39.9 34.7,37 37.6,39.9 38.9,38.5 36,35.6 38.9,32.7 "/>
- <text
- id="text821"
- y="28.091171"
- x="2.7267504"
-
style="font-style:normal;font-weight:normal;font-size:48.81200027px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25422916"
- xml:space="preserve"><tspan
- style="stroke-width:0.25422916"
- y="28.091171"
- x="2.7267504"
- id="tspan819">p</tspan></text>
-</svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-edit.svg
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-edit.svg
deleted file mode 100644
index 71838cbe43..0000000000
---
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/project-edit.svg
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns="http://www.w3.org/2000/svg"
- xml:space="preserve"
- enable-background="new 0 0 42 42"
- viewBox="0 0 42 42"
- height="42px"
- width="42px"
- y="0px"
- x="0px"
- id="Layer_1"
- version="1.1"><metadata
- id="metadata25"><rdf:RDF><cc:Work
- rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title></dc:title></cc:Work></rdf:RDF></metadata>
- <defs
- id="defs23"/>
-
-
- <text
- xml:space="preserve"
-
style="font-style:normal;font-weight:normal;font-size:192px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
- x="56.459057"
- y="19.834593"
- id="text816"><tspan
- id="tspan814"
- x="56.459057"
- y="194.53062" /></text>
-
-
- <ellipse
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"
- id="path2067"
- cx="25.54306"
- cy="34.986797"
- rx="2.0330944"
- ry="2.0063434"/>
- <ellipse
- style="opacity:0.3;fill:#ffd700;fill-opacity:1;paint-order:markers
fill stroke"
- id="path2069"
- cx="29.202629"
- cy="35.715889"
- rx="1.3307527"
- ry="0.22179212"/>
- <ellipse
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"
- id="path2067-1"
- cx="30.903036"
- cy="34.976582"
- rx="2.0330944"
- ry="2.0063434"/>
- <ellipse
-
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:0.72368419;paint-order:markers
fill stroke"
- id="path2067-6"
- cx="36.004257"
- cy="34.976585"
- rx="2.0330944"
- ry="2.0063434"/>
- <text
- id="text821"
- y="28.188118"
- x="2.7025132"
-
style="font-style:normal;font-weight:normal;font-size:48.81200027px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.25422916"
- xml:space="preserve"><tspan
- style="stroke-width:0.25422916"
- y="28.188118"
- x="2.7025132"
- id="tspan819">p</tspan></text>
-</svg>
\ No newline at end of file
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment-list.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment-list.png
index 964a7550e7..9a1dc5f5c2 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment-list.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment-list.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment.png
deleted file mode 100644
index 9bd4b212f3..0000000000
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-environment.png
and /dev/null differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-project-list.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-project-list.png
index 0a5399ad0b..8b9d14e84f 100644
Binary files
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-project-list.png
and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/switch-project-list.png
differ
diff --git
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/the-active-project-at-the-bottom-left.png
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/the-active-project-at-the-bottom-left.png
new file mode 100644
index 0000000000..f20171a82d
Binary files /dev/null and
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/environment/the-active-project-at-the-bottom-left.png
differ
diff --git a/docs/hop-user-manual/modules/ROOT/pages/projects/advanced.adoc
b/docs/hop-user-manual/modules/ROOT/pages/projects/advanced.adoc
index 963160a975..168d5226e4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/projects/advanced.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/projects/advanced.adoc
@@ -95,6 +95,13 @@ Let's take a look at it:
"dataSetsCsvFolder" : "${PROJECT_HOME}/datasets",
"enforcingExecutionInHome" : true,
"parentProjectName" : "default",
+ "parentProjectFolders" : [ {
+ "folder" : "templates",
+ "copyOnce" : true,
+ "copyOnEnable" : false,
+ "overwrite" : false,
+ "exclusionWildcard" : ".*\\.tmp"
+ } ],
"config" : {
"variables" : [ ]
}
@@ -131,6 +138,11 @@ Use the parent variables to reference shared assets
without fragile relative pat
As you can see from the project configuration file (`parentProjectName`), a
project can have a parent from which it will inherit all the metadata objects
as well as all the variables that are defined in it.
When a parent is configured, Hop also sets
`{openvar}PARENT_PROJECT_HOME{closevar}` and
`{openvar}PARENT_PROJECT_NAME{closevar}` so pipelines and environment config
files can point into the parent project home (for example shared pipelines or
secrets kept outside the child repository).
+Optional `parentProjectFolders` entries copy files from those parent folders
into the child project home.
+Each entry can copy once (when the destination is empty), copy every time the
project is enabled, overwrite existing files, and skip files with a Java
regular expression wildcard.
+The child's `project-config.json` and `.git` directories are never overwritten.
+See xref:projects/projects-environments.adoc[Projects and Environments] for
the Project Properties dialog.
+
=== Environment configuration
Hop enviroments and their home folders are stored in the hop configuration
file 'hop-config.json'.
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
index 304e1d872e..aeeff88ed8 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/projects/projects-environments.adoc
@@ -26,50 +26,106 @@ under the License.
== Create a project
-To create a new project click the *Add a new project* button.
+The active project is shown in the bottom left of the Hop Gui status bar
(briefcase icon and project name).
+Click it to open the project menu.
-image:hop-gui/environment/project-add.svg[Add a new project]
+image:hop-gui/environment/the-active-project-at-the-bottom-left.png[The active
project at the bottom left]
+Choose *Add project...* and then *In a folder* to add a project from an
existing folder.
+That opens the project properties dialog.
-This button opens the following dialog:
+image:hop-gui/environment/add-project-menu.png[Add project menu]
-image:hop-gui/environment/create-project-dialog.png[Create a
project,width="90%"]
+*From version control* checks out a project from a Git repository instead.
-.Project Properties
-[id="tab-proj-props",cols="20%,45%,5%,5%,25%",options="header"]
+The project properties fields are grouped on four tabs: *Basic*, *Folders*,
*Parent project*, and *Variables*.
+
+image:hop-gui/environment/create-project-dialog.png[Project Properties Basic
tab,width="90%"]
+
+.Project Properties -- Basic
+[id="tab-proj-props-basic",cols="20%,45%,5%,5%,25%",options="header"]
|===
|Property|Description|Variables Supported|Mandatory|Default
|Name|The project name|Yes|No|
|Home folder|The folder where the project is located|Yes|No|
-|Configuration file (relative path)|The folder where the project's
configuration json is located.|Yes|Yes|
-|Parent project to inherit from|The project to inherit metadata (e.g.
connections) from|Yes|No|default
-|Description|A description for this environment|No|No|
-|Company|The company this environment belongs to|No|No|
-|Department|The department this environment is created for|No|No|
-|Metadata base folder|The folder where this environment's metadata will be
stored|Yes|Yes|{openvar}PROJECT_HOME{closevar}/metadata
+|This project is read only|When checked, Hop does not write
`project-config.json` (archives, HTTP, and similar)|No|No|unchecked
+|Configuration file (relative path)|The project's configuration json, relative
to the home folder.|Yes|Yes|`project-config.json`
+|Description|A description for this project|No|No|
+|Company|The company this project belongs to|No|No|
+|Department|The department this project is created for|No|No|
+|Version|A version label for the project|No|No|
+|Auto-export metadata|Write a single metadata JSON file when the project is
enabled or metadata changes|No|No|unchecked
+|Auto-export metadata filename|Target file for auto-export, relative to the
project home|Yes|No|`metadata.json`
+|===
+
+image:hop-gui/environment/create-project-dialog-folders-tab.png[Project
Properties Folders tab,width="90%"]
+
+.Project Properties -- Folders
+[id="tab-proj-props-folders",cols="20%,45%,5%,5%,25%",options="header"]
+|===
+|Property|Description|Variables Supported|Mandatory|Default
+|Metadata base folder|The folder where this project's metadata will be
stored|Yes|Yes|{openvar}PROJECT_HOME{closevar}/metadata
|Unit test base path|The base path to use for unit tests in the project. If
your `Unit test base path` is defined as
`{openvar}PROJECT_HOME{closevar}/my_pipelines`, unit tests will refer to a
pipeline in this folder as `./my_pipeline.hpl`.
|Yes|Yes|{openvar}PROJECT_HOME{closevar}
-|Data Sets CSV Folder|The folder where this environment's data files will be
stored|Yes|Yes|{openvar}PROJECT_HOME{closevar}/datasets
-|Enforce execution in project home|Give an error when trying to execute a
pipeline or workflow which is not located in the environment home directory or
in a sub-directory|Yes|Yes|checked
-|Project variables to set|A list of variable names, values and variable
descriptions to use with this project|No|Yes|
+|Data Sets CSV Folder|The folder where this project's data files will be
stored|Yes|Yes|{openvar}PROJECT_HOME{closevar}/datasets
+|Enforce execution in project home|Give an error when trying to execute a
pipeline or workflow which is not located in the project home directory or in a
sub-directory|No|No|checked
+|===
+
+image:hop-gui/environment/create-project-dialog-parent-project-tab.png[Project
Properties Parent project tab,width="90%"]
+
+.Project Properties -- Parent project
+[id="tab-proj-props-parent",cols="20%,45%,5%,5%,25%",options="header"]
+|===
+|Property|Description|Variables Supported|Mandatory|Default
+|Parent project to inherit from|The project to inherit metadata (e.g.
connections) and variables from|Yes|No|default
+|Parent project folders to synchronize|Zero or more folders to copy from the
parent project home into this project. See below.|Yes|No|
+|===
+
+Each row in *Parent project folders to synchronize* copies that folder (or `.`
for the parent root) into the same relative path in the child project.
+All files are copied, not only pipelines and workflows.
+
+.Parent project folder columns
+[cols="20%,80%",options="header"]
+|===
+|Column|Description
+|Folder|Path relative to the parent project home. `.` copies the parent root.
+|Copy once|Copy when the destination folder is missing or empty (a newly
created project home that only contains `project-config.json` is treated as
empty).
+|Copy on enable|Copy every time this project is enabled (Hop Gui, `hop-run`,
and other enable paths).
+|Overwrite|Replace files that already exist in the child project. When
unchecked, existing files are kept.
+|Exclusion (regular expression)|Java regular expression matched against the
file base name and the path relative to the copied folder. Example: `.*\.tmp`
or `secret/.*`. Invalid expressions skip that row so excluded files are not
copied by accident.
+|===
+
+The child's `project-config.json` and any `.git` directory are never copied or
overwritten, even with overwrite enabled.
+If both *Copy once* and *Copy on enable* are checked, copy-on-enable wins.
+If neither is checked, the row is skipped.
+
+image:hop-gui/environment/create-project-dialog-variables-tab.png[Project
Properties Variables tab,width="90%"]
+
+.Project Properties -- Variables
+[id="tab-proj-props-variables",cols="20%,45%,5%,5%,25%",options="header"]
+|===
+|Property|Description|Variables Supported|Mandatory|Default
+|Project variables to set|A list of variable names, values and variable
descriptions to use with this project|No|No|
|===
After creating a project the user interface will switch to it and ask if you
want to create an environment.
== Create an environment
-To create a new environment click the *Add a new environment* button.
+The active environment is shown next to the project in the bottom left of the
Hop Gui status bar (cube icon).
+Click it to open the environment menu, then choose *Add environment...*.
-image:hop-gui/environment/environment-add.svg[Environment]
+image:hop-gui/environment/environment-add.png[Add environment menu]
-This button opens the following dialogue:
+This opens the environment properties dialog.
+The fields are grouped on tabs: *General*, *Configuration files*, and when the
matching plugins are installed *Marketplace plugins* and *System resources*.
-image:hop-gui/environment/create-environment-dialogue.png[Create an
environment,width="45%"]
+image:hop-gui/environment/environment-dialog-general-tab.png[Environment
Properties General tab,width="90%"]
-.Environment Properties
+.Environment Properties -- General
[id="tab-env-props",cols="20%,45%,5%,5%,25%",options="header"]
|===
|Property|Description|Variables Supported|Mandatory|Default
-|Name|The environment name|No|No|The last created project
+|Name|The environment name|No|No|Suggested from the project name and purpose
|Purpose a|The purpose of this environment. A purpose is an indication of the
environment type. You can add your own purposes in addition to the predefined
ones:
* Development
@@ -80,24 +136,56 @@
image:hop-gui/environment/create-environment-dialogue.png[Create an environment,
* Common build
* ...|No|No|
|Project|The project to which this environment belongs|No|No|The last created
project
-|Configuration files|Files to use as configuration for this environment|No|No|
+|Canvas text|Large text drawn in the top-right of pipeline and workflow
canvases when this environment is active|Yes|No|
+|===
+
+image:hop-gui/environment/environment-dialog-configuration-files-tab.png[Environment
Properties Configuration files tab,width="90%"]
+
+.Environment Properties -- Configuration files
+[id="tab-env-props-files",cols="20%,45%,5%,5%,25%",options="header"]
+|===
+|Property|Description|Variables Supported|Mandatory|Default
+|Configuration files|One or more files that define environment-specific
variables|Yes|No|
+|Import variables|Scan the project for `{openvar}VARIABLE{closevar}`
expressions and add missing names to a configuration file|No|No|
+|Select...|Add an existing configuration file|No|No|
+|New...|Create a new configuration file and add it to the environment|No|No|
+|Edit...|Open the selected configuration file to set variable names, values
and descriptions|No|No|
|===
-Each environment will contain one or more environment files where you manage
environment-specific variables.
+Each environment contains one or more configuration files where you manage
environment-specific variables.
-The edit button opens the environment file and lets you add variable key-value
pairs.
+image:hop-gui/environment/environment-files.png[Environment configuration
files,width="90%"]
-image::hop-gui/environment/environment-variables.png[Environment Variables,
width="80%"]
+*Edit...* opens the described variables dialog for the selected file:
+
+image::hop-gui/environment/environment-variables.png[Environment
Variables,width="80%"]
When you browse for a file or directory in Hop Gui with a project active,
paths under a matching path-like variable are rewritten automatically. For
example, `{openvar}PROJECT_HOME{closevar}` is used for files under the project
home, and an environment variable such as `SOURCE_FILES=/data/incoming`
rewrites a selection under that folder to `{openvar}SOURCE_FILES{closevar}/…`.
If more than one variable matches, the longest (most specific) path wins. The
same applies to path variables defi [...]
After creating an environment the user interface will switch to it.
+== Marketplace plugins
+
+When the xref:hop-tools/hop-marketplace.adoc[Marketplace] plugin is installed,
the environment properties dialog includes a *Marketplace plugins* tab.
+These options control whether Hop checks a `hop-env` file when this
environment is enabled.
+
+image:hop-gui/environment/environment-dialog-marketplace-plugins-tab.png[Environment
Properties Marketplace plugins tab,width="90%"]
+
+Typical options:
+
+* *Environment file* — path to a `hop-env.yaml` (variables allowed, for
example under the project home)
+* *On enable* — `off` (no check), `warn` (log or dialog on drift), or
`enforce` (fail enabling the environment if the install does not match)
+* *Strict* — treat extra marketplace plugins as drift
+* *Auto-apply missing plugins on enable* — optional; off by default
+
+See xref:hop-tools/hop-marketplace.adoc[Hop Marketplace] for the full
description of lifecycle environment plugin checks.
+
== System resource requirements
-Lifecycle environments can declare *system resource requirements* for the host
where Hop runs. Open the environment properties dialog and select the *System
resources* tab.
+Lifecycle environments can declare *system resource requirements* for the host
where Hop runs.
+Open the environment properties dialog and select the *System resources* tab.
-image::projects-environment-system-resources.png[Lifecycle environment System
resources tab,75%,align="left"]
+image:hop-gui/environment/environment-dialog-system-resources-tab.png[Environment
Properties System resources tab,width="90%"]
When the environment is enabled (Hop Gui, `hop-run -e`, `hop-server -e`, and
other tools that call project enablement), Hop can check:
@@ -122,38 +210,30 @@ These checks run on the JVM that enables the environment.
They do not validate r
== Switch between projects and environments
-To switch between projects the projects list can be used from the main Hop Gui
toolbar.
+Click the project indicator at the bottom left and choose a recent project
from the menu, or *More projects...* to pick from a list.
-image::hop-gui/environment/switch-project-list.png[Switch project]
+image:hop-gui/environment/switch-project-list.png[Switch project]
-After switching to a project the environments list will be updated to the
environments which belong to this project.
+After switching to a project, click the environment indicator (cube icon) next
to the project name and choose an environment from the menu.
All open files will be restored, including their zoom level and other UI
settings.
-image::hop-gui/environment/switch-environment-list.png[Switch environment]
+image:hop-gui/environment/switch-environment-list.png[Switch environment]
== Edit and delete projects
-To edit an existing project click the *Edit the selected project* button.
-
-image:hop-gui/environment/project-edit.svg[Edit the selected project]
-
-Clicking this button will re-open the project dialogue where changes can be
made as described in <<tab-proj-props>>.
-
-To delete a project click the *Delete the selected project* button.
-
-image:hop-gui/environment/project-delete.svg[Delete the selected project]
-
-== Editing and deleting environments
+Click the project indicator at the bottom left, then choose *Edit project...*
to re-open the project properties dialog described above.
-To edit an existing environment click the *Edit the selected environment*
button.
+Choose *Delete project...* from the same menu to remove the project from the
Hop configuration.
-image:hop-gui/environment/environment-edit.svg[Edit the selected environment]
+== Edit, duplicate and delete environments
-Clicking this button will re-open the project dialogue where changes can be
made as described in <<tab-env-props>>.
+Click the environment indicator at the bottom left to open the environment
menu.
-To delete an environment click the *Delete the selected environment* button.
+image:hop-gui/environment/edit-environment-menu.png[Edit environment menu]
-image:hop-gui/environment/environment-delete.svg[Delete the selected
environment]
+* *Edit environment...* re-opens the environment properties dialog described
above.
+* *Duplicate environment...* creates a copy of the selected environment.
+* *Delete environment...* removes the environment from the Hop configuration.
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
index 7b7df2a3cd..67769b52fd 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironmentDialog.java
@@ -36,6 +36,7 @@ import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.projects.config.ProjectsConfig;
import org.apache.hop.projects.config.ProjectsConfigSingleton;
import org.apache.hop.projects.project.ProjectConfig;
+import org.apache.hop.projects.util.Defaults;
import org.apache.hop.ui.core.ConstUi;
import org.apache.hop.ui.core.PropsUi;
import org.apache.hop.ui.core.dialog.AttributesDialogExtension;
@@ -50,6 +51,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
import org.apache.hop.ui.core.widget.TableView;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.apache.hop.ui.util.HelpUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
@@ -153,6 +155,7 @@ public class LifecycleEnvironmentDialog extends Dialog {
wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
wCancel.addListener(SWT.Selection, event -> cancel());
BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOK,
wCancel}, margin * 3, null);
+ HelpUtils.createHelpButton(shell,
Const.getDocUrl(Defaults.DOCUMENTATION_URI));
CTabFolder wTabFolder = new CTabFolder(shell, SWT.BORDER);
PropsUi.setLook(wTabFolder);
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolder.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolder.java
new file mode 100644
index 0000000000..521d69754c
--- /dev/null
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolder.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.project;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * A folder in a parent project to copy into a child project home. Stored in
{@code
+ * project-config.json} as entries of {@code parentProjectFolders}.
+ */
+@Getter
+@Setter
+public class ParentProjectFolder {
+
+ /** Path relative to the parent project home. Empty or {@code .} means the
parent root. */
+ private String folder;
+
+ /**
+ * Copy when the destination folder is missing or empty. Subsequent enables
skip this mapping
+ * unless {@link #copyOnEnable} is also true.
+ */
+ private boolean copyOnce;
+
+ /** Copy every time the child project is enabled. */
+ private boolean copyOnEnable;
+
+ /** Replace existing destination files when this mapping runs. */
+ private boolean overwrite;
+
+ /**
+ * Java regular expression (Hop wildcard) matched against the file base name
and the path relative
+ * to the copied folder. Empty means no extra exclusions.
+ */
+ private String exclusionWildcard;
+}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizer.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizer.java
new file mode 100644
index 0000000000..9f9ab22609
--- /dev/null
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizer.java
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.project;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.NameScope;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.projects.util.ProjectsUtil;
+
+/**
+ * Copies configured folders from a parent project into the child project home
using Hop VFS.
+ * Invoked when a project is enabled. Never copies a whole tree with {@code
copyFrom} (that empties
+ * an existing destination).
+ */
+public final class ParentProjectFolderSynchronizer {
+
+ private ParentProjectFolderSynchronizer() {
+ // utility
+ }
+
+ /**
+ * Apply {@link Project#getParentProjectFolders()} for the given child
project. Failures on a
+ * single mapping are logged and the rest continue; this method does not
throw for copy problems.
+ *
+ * @param log log channel (may be null)
+ * @param project child project
+ * @param projectConfig child project registration
+ * @param variables variable space with {@code PROJECT_HOME} and {@code
PARENT_PROJECT_HOME} set
+ */
+ public static void synchronize(
+ ILogChannel log, Project project, ProjectConfig projectConfig,
IVariables variables) {
+ if (project == null || projectConfig == null || variables == null) {
+ return;
+ }
+ if (projectConfig.isReadOnly()) {
+ logDetailed(log, "Skipping parent folder sync for read-only project");
+ return;
+ }
+ List<ParentProjectFolder> folders = project.getParentProjectFolders();
+ if (folders == null || folders.isEmpty()) {
+ return;
+ }
+ if (StringUtils.isEmpty(project.getParentProjectName())) {
+ return;
+ }
+
+ String childHomePath =
variables.getVariable(ProjectsUtil.VARIABLE_PROJECT_HOME);
+ String parentHomePath =
variables.getVariable(ProjectsUtil.VARIABLE_PARENT_PROJECT_HOME);
+ if (StringUtils.isEmpty(childHomePath) ||
StringUtils.isEmpty(parentHomePath)) {
+ logError(
+ log,
+ "Cannot synchronize parent project folders: PROJECT_HOME or
PARENT_PROJECT_HOME is not set");
+ return;
+ }
+
+ try (FileObject childHome = HopVfs.getFileObject(childHomePath);
+ FileObject parentHome = HopVfs.getFileObject(parentHomePath)) {
+ if (!parentHome.exists()) {
+ logError(log, "Parent project home does not exist: " + parentHomePath);
+ return;
+ }
+
+ FileObject childConfigFile = resolveChildConfigFile(projectConfig,
variables, childHome);
+ try {
+ for (ParentProjectFolder mapping : folders) {
+ if (mapping == null) {
+ continue;
+ }
+ try {
+ synchronizeMapping(log, mapping, parentHome, childHome,
childConfigFile);
+ } catch (Exception e) {
+ logError(
+ log,
+ "Error synchronizing parent folder '"
+ + Const.NVL(mapping.getFolder(), "")
+ + "' into project '"
+ + projectConfig.getProjectName()
+ + "'",
+ e);
+ }
+ }
+ } finally {
+ closeQuietly(childConfigFile);
+ }
+ } catch (Exception e) {
+ logError(log, "Error opening project homes for parent folder
synchronization", e);
+ }
+ }
+
+ static void synchronizeMapping(
+ ILogChannel log,
+ ParentProjectFolder mapping,
+ FileObject parentHome,
+ FileObject childHome,
+ FileObject childConfigFile)
+ throws FileSystemException, HopException {
+
+ if (!mapping.isCopyOnce() && !mapping.isCopyOnEnable()) {
+ return;
+ }
+
+ Pattern exclusion = compileExclusion(log, mapping);
+ if (StringUtils.isNotBlank(mapping.getExclusionWildcard()) && exclusion ==
null) {
+ // Invalid regex: skip the mapping so files the user meant to exclude
are not copied.
+ return;
+ }
+
+ FileObject source = null;
+ FileObject dest = null;
+ try {
+ source = resolveUnderHome(parentHome, mapping.getFolder());
+ dest = resolveUnderHome(childHome, mapping.getFolder());
+
+ if (!isUnder(dest, childHome)) {
+ logError(
+ log,
+ "Refusing to copy parent folder '"
+ + Const.NVL(mapping.getFolder(), "")
+ + "': destination is outside the child project home");
+ return;
+ }
+
+ if (!source.exists()) {
+ logError(log, "Parent project folder does not exist: " +
source.getName().getURI());
+ return;
+ }
+
+ boolean destEmpty = isMissingOrEmpty(dest, childConfigFile);
+ boolean runCopyOnce = mapping.isCopyOnce() && destEmpty;
+ boolean runOnEnable = mapping.isCopyOnEnable();
+ if (!runOnEnable && !runCopyOnce) {
+ logDetailed(
+ log,
+ "Skipping copy-once parent folder '"
+ + Const.NVL(mapping.getFolder(), "")
+ + "': destination already has files");
+ return;
+ }
+
+ copyTree(log, source, dest, "", mapping.isOverwrite(), exclusion,
childConfigFile);
+ } catch (FileSystemException e) {
+ logError(
+ log,
+ "Parent folder '"
+ + Const.NVL(mapping.getFolder(), "")
+ + "' is outside the project home (path traversal rejected)",
+ e);
+ } finally {
+ if (source != parentHome) {
+ closeQuietly(source);
+ }
+ if (dest != childHome) {
+ closeQuietly(dest);
+ }
+ }
+ }
+
+ static FileObject resolveUnderHome(FileObject home, String folder) throws
FileSystemException {
+ if (home == null) {
+ throw new FileSystemException("Project home is not set");
+ }
+ String relative = folder == null ? "" : folder.trim();
+ if (relative.isEmpty() || ".".equals(relative) || "./".equals(relative)) {
+ return home;
+ }
+ // NameScope.DESCENDENT_OR_SELF rejects ".." and absolute paths that leave
home.
+ return home.resolveFile(relative, NameScope.DESCENDENT_OR_SELF);
+ }
+
+ static boolean isMissingOrEmpty(FileObject dest, FileObject childConfigFile)
+ throws FileSystemException {
+ if (dest == null || !dest.exists()) {
+ return true;
+ }
+ if (dest.getType() != FileType.FOLDER) {
+ return false;
+ }
+ FileObject[] children = dest.getChildren();
+ try {
+ if (children == null || children.length == 0) {
+ return true;
+ }
+ for (FileObject child : children) {
+ if (".git".equals(child.getName().getBaseName())) {
+ continue;
+ }
+ if (isProtectedConfigFile(child, childConfigFile)) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ } finally {
+ closeQuietly(children);
+ }
+ }
+
+ static boolean isUnder(FileObject file, FileObject directory) throws
FileSystemException {
+ if (file == null || directory == null) {
+ return false;
+ }
+ String filePath = file.getName().getURI();
+ String directoryPath = directory.getName().getURI();
+ if (filePath.equals(directoryPath)) {
+ return true;
+ }
+ String prefix = directoryPath.endsWith("/") ? directoryPath :
directoryPath + "/";
+ return filePath.startsWith(prefix);
+ }
+
+ static Pattern compileExclusion(ILogChannel log, ParentProjectFolder
mapping) {
+ String wildcard = mapping.getExclusionWildcard();
+ if (StringUtils.isBlank(wildcard)) {
+ return null;
+ }
+ try {
+ return Pattern.compile(wildcard.trim());
+ } catch (PatternSyntaxException e) {
+ logError(
+ log,
+ "Invalid exclusion regular expression '"
+ + wildcard
+ + "' for parent folder '"
+ + Const.NVL(mapping.getFolder(), "")
+ + "': "
+ + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * True when the file should be skipped because of the user regular
expression. Matches the base
+ * name and the slash-separated path relative to the copied folder (not a
VFS URI), same idea as
+ * other Hop file wildcards.
+ */
+ static boolean matchesExclusion(Pattern exclusion, String baseName, String
relativePath) {
+ if (exclusion == null) {
+ return false;
+ }
+ if (baseName != null && exclusion.matcher(baseName).matches()) {
+ return true;
+ }
+ if (relativePath != null
+ && !relativePath.isEmpty()
+ && exclusion.matcher(relativePath).matches()) {
+ return true;
+ }
+ return false;
+ }
+
+ static boolean isGitPath(String baseName, String relativePath) {
+ if (".git".equals(baseName)) {
+ return true;
+ }
+ if (relativePath == null || relativePath.isEmpty()) {
+ return false;
+ }
+ return ".git".equals(relativePath)
+ || relativePath.startsWith(".git/")
+ || relativePath.contains("/.git/")
+ || relativePath.endsWith("/.git");
+ }
+
+ static boolean isProtectedConfigFile(FileObject dest, FileObject
childConfigFile)
+ throws FileSystemException {
+ if (dest == null || childConfigFile == null) {
+ return false;
+ }
+ return dest.getName().getURI().equals(childConfigFile.getName().getURI());
+ }
+
+ private static void copyTree(
+ ILogChannel log,
+ FileObject source,
+ FileObject dest,
+ String relativePath,
+ boolean overwrite,
+ Pattern exclusion,
+ FileObject childConfigFile)
+ throws FileSystemException, HopException {
+
+ String baseName = source.getName().getBaseName();
+ if (isGitPath(baseName, relativePath)) {
+ return;
+ }
+
+ if (source.getType() == FileType.FOLDER) {
+ if (!dest.exists()) {
+ dest.createFolder();
+ }
+ FileObject[] children = source.getChildren();
+ try {
+ if (children == null) {
+ return;
+ }
+ for (FileObject child : children) {
+ String childRelative =
+ relativePath.isEmpty()
+ ? child.getName().getBaseName()
+ : relativePath + "/" + child.getName().getBaseName();
+ FileObject destChild =
dest.resolveFile(child.getName().getBaseName(), NameScope.CHILD);
+ try {
+ copyTree(log, child, destChild, childRelative, overwrite,
exclusion, childConfigFile);
+ } finally {
+ closeQuietly(destChild);
+ }
+ }
+ } finally {
+ closeQuietly(children);
+ }
+ return;
+ }
+
+ if (isProtectedConfigFile(dest, childConfigFile)) {
+ logDetailed(log, "Skipping child project configuration file: " +
dest.getName().getURI());
+ return;
+ }
+ if (matchesExclusion(exclusion, baseName, relativePath)) {
+ logDetailed(log, "Skipping excluded file: " + relativePath);
+ return;
+ }
+ if (dest.exists() && !overwrite) {
+ logDetailed(log, "Keeping existing file: " + dest.getName().getURI());
+ return;
+ }
+
+ FileObject destParent = dest.getParent();
+ if (destParent != null && !destParent.exists()) {
+ destParent.createFolder();
+ }
+
+ try (InputStream in = HopVfs.getInputStream(source);
+ OutputStream out = HopVfs.getOutputStream(dest, false)) {
+ in.transferTo(out);
+ } catch (Exception e) {
+ throw new HopException("Error copying '" + source.getName().getURI() +
"'", e);
+ }
+ logDetailed(log, "Copied parent project file: " + relativePath);
+ }
+
+ private static FileObject resolveChildConfigFile(
+ ProjectConfig projectConfig, IVariables variables, FileObject childHome)
{
+ try {
+ String configPath =
projectConfig.getActualProjectConfigFilename(variables);
+ if (StringUtils.isNotEmpty(configPath)) {
+ return HopVfs.getFileObject(configPath);
+ }
+ } catch (Exception e) {
+ // Fall through to home + relative config filename
+ }
+ try {
+ String configFilename = projectConfig.getConfigFilename();
+ if (StringUtils.isEmpty(configFilename)) {
+ return null;
+ }
+ return childHome.resolveFile(configFilename,
NameScope.DESCENDENT_OR_SELF);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static void logError(ILogChannel log, String message) {
+ if (log == null) {
+ return;
+ }
+ try {
+ log.logError(message);
+ } catch (Exception ignored) {
+ // Logging must not abort parent-folder sync (HopLogStore may not be
initialized).
+ }
+ }
+
+ private static void logError(ILogChannel log, String message, Exception e) {
+ if (log == null) {
+ return;
+ }
+ try {
+ log.logError(message, e);
+ } catch (Exception ignored) {
+ // Logging must not abort parent-folder sync (HopLogStore may not be
initialized).
+ }
+ }
+
+ private static void logDetailed(ILogChannel log, String message) {
+ if (log == null) {
+ return;
+ }
+ try {
+ if (log.isDetailed()) {
+ log.logDetailed(message);
+ }
+ } catch (Exception ignored) {
+ // ignore
+ }
+ }
+
+ private static void closeQuietly(FileObject file) {
+ if (file != null) {
+ try {
+ file.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ private static void closeQuietly(FileObject[] files) {
+ if (files == null) {
+ return;
+ }
+ for (FileObject file : files) {
+ closeQuietly(file);
+ }
+ }
+}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/Project.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/Project.java
index 829fbf7f7d..522e66f97f 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/Project.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/Project.java
@@ -99,6 +99,13 @@ public class Project extends ConfigFile implements
IConfigFile {
private String autoExportMetadataFilename;
private String parentProjectName;
+
+ /**
+ * Folders to copy from the parent project home into this project. Empty
means no file
+ * synchronization; metadata/variable inheritance is independent of this
list.
+ */
+ private List<ParentProjectFolder> parentProjectFolders;
+
@JsonIgnore private MultiMetadataProvider metadataProvider;
@JsonIgnore private List<Path> pipelinePaths;
@JsonIgnore private List<Path> workflowPaths;
@@ -113,6 +120,7 @@ public class Project extends ConfigFile implements
IConfigFile {
enforcingExecutionInHome = true;
autoExportMetadata = false;
autoExportMetadataFilename = "";
+ parentProjectFolders = new ArrayList<>();
}
public Project(String configFilename) {
@@ -172,6 +180,10 @@ public class Project extends ConfigFile implements
IConfigFile {
this.autoExportMetadataFilename = project.autoExportMetadataFilename;
this.configMap = project.configMap;
this.parentProjectName = project.parentProjectName;
+ this.parentProjectFolders =
+ project.parentProjectFolders != null
+ ? new ArrayList<>(project.parentProjectFolders)
+ : new ArrayList<>();
} catch (Exception e) {
throw new HopException(
"Error saving project configuration to file '" + configFilename +
"'", e);
@@ -645,6 +657,13 @@ public class Project extends ConfigFile implements
IConfigFile {
return resultStrings;
}
+ public List<ParentProjectFolder> getParentProjectFolders() {
+ if (parentProjectFolders == null) {
+ parentProjectFolders = new ArrayList<>();
+ }
+ return parentProjectFolders;
+ }
+
/**
* Gets configFilename
*
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
index 7bbbe319fd..e77b5f59fd 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/project/ProjectDialog.java
@@ -51,9 +51,10 @@ import org.apache.hop.ui.core.widget.TableView;
import org.apache.hop.ui.core.widget.TextVar;
import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.apache.hop.ui.util.HelpUtils;
import org.eclipse.swt.SWT;
-import org.eclipse.swt.custom.ScrolledComposite;
-import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
@@ -70,6 +71,7 @@ import org.eclipse.swt.widgets.Text;
public class ProjectDialog extends Dialog {
private static final Class<?> PKG = ProjectDialog.class;
public static final String CONST_PROJECT = "Project '";
+ private static final String[] YES_NO = {"Y", "N"};
private final Project project;
private final ProjectConfig projectConfig;
@@ -97,6 +99,7 @@ public class ProjectDialog extends Dialog {
private TextVar wDataSetCsvFolder;
private Button wEnforceHomeExecution;
private TableView wVariables;
+ private TableView wParentFolders;
private final IVariables variables;
@@ -152,7 +155,6 @@ public class ProjectDialog extends Dialog {
PropsUi.setLook(shell);
int margin = PropsUi.getMargin() + 2;
- int middle = props.getMiddlePct();
FormLayout formLayout = new FormLayout();
formLayout.marginWidth = PropsUi.getFormMargin();
@@ -161,8 +163,6 @@ public class ProjectDialog extends Dialog {
shell.setLayout(formLayout);
shell.setText(BaseMessages.getString(PKG, "ProjectDialog.Shell.Name"));
- // Buttons go at the bottom of the dialog
- //
Button wOk = new Button(shell, SWT.PUSH);
wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
wOk.addListener(SWT.Selection, event -> ok());
@@ -170,24 +170,63 @@ public class ProjectDialog extends Dialog {
wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
wCancel.addListener(SWT.Selection, event -> cancel());
BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk,
wCancel}, margin * 3, null);
+ HelpUtils.createHelpButton(shell,
Const.getDocUrl(Defaults.DOCUMENTATION_URI));
+
+ CTabFolder wTabFolder = new CTabFolder(shell, SWT.BORDER);
+ PropsUi.setLook(wTabFolder);
+ FormData fdTabs = new FormData();
+ fdTabs.left = new FormAttachment(0, 0);
+ fdTabs.top = new FormAttachment(0, 0);
+ fdTabs.right = new FormAttachment(100, 0);
+ fdTabs.bottom = new FormAttachment(wOk, -margin * 2);
+ wTabFolder.setLayoutData(fdTabs);
+
+ createBasicTab(wTabFolder, margin);
+ createFoldersTab(wTabFolder, margin);
+ createParentProjectTab(wTabFolder, margin);
+ createVariablesTab(wTabFolder, margin);
+
+ wParentProject.addModifyListener(
+ e -> {
+ needingProjectRefresh = true;
+ updateParentFolderWidgets();
+ });
+ wHome.addModifyListener(
+ e -> {
+ needingProjectRefresh = true;
+ autoSetReadOnlyFromHome();
+ });
- ScrolledComposite scroll = new ScrolledComposite(shell, SWT.V_SCROLL);
- scroll.setLayout(new FillLayout());
- scroll.setExpandHorizontal(true);
- scroll.setExpandVertical(true);
- PropsUi.setLook(scroll);
- shell.setLayoutData(scroll);
+ getData();
+ updateReadOnlyWidgets();
+ updateAutoExportMetadataWidgets();
+ updateParentFolderWidgets();
- FormData fd = new FormData();
- fd.left = new FormAttachment(0, 0);
- fd.right = new FormAttachment(100, 0);
- fd.top = new FormAttachment(0, 0);
- fd.bottom = new FormAttachment(wOk, 0);
- scroll.setLayoutData(fd);
+ wTabFolder.setSelection(0);
+ shell.setMinimumSize(700, 450);
+ shell.setDefaultButton(wOk);
+ wName.setFocus();
+ BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
- Composite comp = new Composite(scroll, SWT.NONE);
- comp.setLayout(new FormLayout());
+ return returnValue;
+ }
+
+ private Composite createTab(CTabFolder folder, String messageKey) {
+ CTabItem tab = new CTabItem(folder, SWT.NONE);
+ tab.setText(BaseMessages.getString(PKG, messageKey));
+ Composite comp = new Composite(folder, SWT.NONE);
PropsUi.setLook(comp);
+ FormLayout layout = new FormLayout();
+ layout.marginWidth = PropsUi.getFormMargin();
+ layout.marginHeight = PropsUi.getFormMargin();
+ comp.setLayout(layout);
+ tab.setControl(comp);
+ return comp;
+ }
+
+ private void createBasicTab(CTabFolder folder, int margin) {
+ Composite comp = createTab(folder, "ProjectDialog.Tab.Basic");
+ int middle = props.getMiddlePct();
Label wlName = new Label(comp, SWT.RIGHT);
PropsUi.setLook(wlName);
@@ -195,13 +234,13 @@ public class ProjectDialog extends Dialog {
FormData fdlName = new FormData();
fdlName.left = new FormAttachment(0, 0);
fdlName.right = new FormAttachment(middle, 0);
- fdlName.top = new FormAttachment(0, margin * 2);
+ fdlName.top = new FormAttachment(0, margin);
wlName.setLayoutData(fdlName);
wName = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
PropsUi.setLook(wName);
FormData fdName = new FormData();
fdName.left = new FormAttachment(middle, margin);
- fdName.right = new FormAttachment(99, 0);
+ fdName.right = new FormAttachment(100, 0);
fdName.top = new FormAttachment(wlName, 0, SWT.CENTER);
wName.setLayoutData(fdName);
Control lastControl = wName;
@@ -218,7 +257,7 @@ public class ProjectDialog extends Dialog {
PropsUi.setLook(wbHome);
wbHome.setText(BaseMessages.getString(PKG, "ProjectDialog.Button.Browse"));
FormData fdbHome = new FormData();
- fdbHome.right = new FormAttachment(99, 0);
+ fdbHome.right = new FormAttachment(100, 0);
fdbHome.top = new FormAttachment(wlHome, 0, SWT.CENTER);
wbHome.setLayoutData(fdbHome);
wbHome.addListener(SWT.Selection, this::browseHomeFolder);
@@ -231,14 +270,12 @@ public class ProjectDialog extends Dialog {
wHome.setLayoutData(fdHome);
lastControl = wHome;
- // Read-only option below the home folder path (auto-enabled for archive
URIs)
- //
wReadOnly = new Button(comp, SWT.CHECK | SWT.LEFT);
PropsUi.setLook(wReadOnly);
wReadOnly.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.ReadOnly"));
FormData fdReadOnly = new FormData();
fdReadOnly.left = new FormAttachment(middle, margin);
- fdReadOnly.right = new FormAttachment(99, 0);
+ fdReadOnly.right = new FormAttachment(100, 0);
fdReadOnly.top = new FormAttachment(lastControl, margin);
wReadOnly.setLayoutData(fdReadOnly);
wReadOnly.addListener(SWT.Selection, e -> updateReadOnlyWidgets());
@@ -256,7 +293,7 @@ public class ProjectDialog extends Dialog {
PropsUi.setLook(wbConfigFile);
wbConfigFile.setText(BaseMessages.getString(PKG,
"ProjectDialog.Button.Browse"));
FormData fdbConfigFile = new FormData();
- fdbConfigFile.right = new FormAttachment(99, 0);
+ fdbConfigFile.right = new FormAttachment(100, 0);
fdbConfigFile.top = new FormAttachment(wlConfigFile, 0, SWT.CENTER);
wbConfigFile.setLayoutData(fdbConfigFile);
wbConfigFile.addListener(SWT.Selection, this::browseConfigFolder);
@@ -269,109 +306,38 @@ public class ProjectDialog extends Dialog {
wConfigFile.setLayoutData(fdConfigFile);
lastControl = wConfigFile;
- Label wlParentProject = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlParentProject);
- wlParentProject.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.ParentProject"));
- FormData fdlParentProject = new FormData();
- fdlParentProject.left = new FormAttachment(0, 0);
- fdlParentProject.right = new FormAttachment(middle, 0);
- fdlParentProject.top = new FormAttachment(lastControl, margin);
- wlParentProject.setLayoutData(fdlParentProject);
- wParentProject = new ComboVar(variables, comp, SWT.SINGLE | SWT.BORDER |
SWT.LEFT);
- PropsUi.setLook(wParentProject);
- FormData fdParentProject = new FormData();
- fdParentProject.left = new FormAttachment(middle, margin);
- fdParentProject.right = new FormAttachment(99, 0);
- fdParentProject.top = new FormAttachment(wlParentProject, 0, SWT.CENTER);
- wParentProject.setLayoutData(fdParentProject);
- lastControl = wParentProject;
-
- Label wlDescription = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlDescription);
- wlDescription.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Description"));
- FormData fdlDescription = new FormData();
- fdlDescription.left = new FormAttachment(0, 0);
- fdlDescription.right = new FormAttachment(middle, 0);
- fdlDescription.top = new FormAttachment(lastControl, margin);
- wlDescription.setLayoutData(fdlDescription);
- wDescription = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
- PropsUi.setLook(wDescription);
- FormData fdDescription = new FormData();
- fdDescription.left = new FormAttachment(middle, margin);
- fdDescription.right = new FormAttachment(99, 0);
- fdDescription.top = new FormAttachment(wlDescription, 0, SWT.CENTER);
- wDescription.setLayoutData(fdDescription);
- lastControl = wDescription;
-
- Label wlCompany = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlCompany);
- wlCompany.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Company"));
- FormData fdlCompany = new FormData();
- fdlCompany.left = new FormAttachment(0, 0);
- fdlCompany.right = new FormAttachment(middle, 0);
- fdlCompany.top = new FormAttachment(lastControl, margin);
- wlCompany.setLayoutData(fdlCompany);
- wCompany = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
- PropsUi.setLook(wCompany);
- FormData fdCompany = new FormData();
- fdCompany.left = new FormAttachment(middle, margin);
- fdCompany.right = new FormAttachment(99, 0);
- fdCompany.top = new FormAttachment(wlCompany, 0, SWT.CENTER);
- wCompany.setLayoutData(fdCompany);
- lastControl = wCompany;
-
- Label wlDepartment = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlDepartment);
- wlDepartment.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Department"));
- FormData fdlDepartment = new FormData();
- fdlDepartment.left = new FormAttachment(0, 0);
- fdlDepartment.right = new FormAttachment(middle, 0);
- fdlDepartment.top = new FormAttachment(lastControl, margin);
- wlDepartment.setLayoutData(fdlDepartment);
- wDepartment = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
- PropsUi.setLook(wDepartment);
- FormData fdDepartment = new FormData();
- fdDepartment.left = new FormAttachment(middle, margin);
- fdDepartment.right = new FormAttachment(99, 0);
- fdDepartment.top = new FormAttachment(wlDepartment, 0, SWT.CENTER);
- wDepartment.setLayoutData(fdDepartment);
- lastControl = wDepartment;
-
- Label wlVersion = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlVersion);
- wlVersion.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.Version"));
- FormData fdlVersion = new FormData();
- fdlVersion.left = new FormAttachment(0, 0);
- fdlVersion.right = new FormAttachment(middle, 0);
- fdlVersion.top = new FormAttachment(lastControl, margin);
- wlVersion.setLayoutData(fdlVersion);
- wVersion = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT);
- PropsUi.setLook(wVersion);
- FormData fdVersion = new FormData();
- fdVersion.left = new FormAttachment(middle, margin);
- fdVersion.right = new FormAttachment(99, 0);
- fdVersion.top = new FormAttachment(wlVersion, 0, SWT.CENTER);
- wVersion.setLayoutData(fdVersion);
- lastControl = wVersion;
-
- Label wlMetadataBaseFolder = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlMetadataBaseFolder);
- wlMetadataBaseFolder.setText(
- BaseMessages.getString(PKG, "ProjectDialog.Label.MetadataBaseFolder"));
- FormData fdlMetadataBaseFolder = new FormData();
- fdlMetadataBaseFolder.left = new FormAttachment(0, 0);
- fdlMetadataBaseFolder.right = new FormAttachment(middle, 0);
- fdlMetadataBaseFolder.top = new FormAttachment(lastControl, margin);
- wlMetadataBaseFolder.setLayoutData(fdlMetadataBaseFolder);
- wMetadataBaseFolder = new TextVar(variables, comp, SWT.SINGLE | SWT.BORDER
| SWT.LEFT);
- PropsUi.setLook(wMetadataBaseFolder);
- FormData fdMetadataBaseFolder = new FormData();
- fdMetadataBaseFolder.left = new FormAttachment(middle, margin);
- fdMetadataBaseFolder.right = new FormAttachment(99, 0);
- fdMetadataBaseFolder.top = new FormAttachment(wlMetadataBaseFolder, 0,
SWT.CENTER);
- wMetadataBaseFolder.setLayoutData(fdMetadataBaseFolder);
- wMetadataBaseFolder.addModifyListener(e -> updateIVariables());
- lastControl = wMetadataBaseFolder;
+ lastControl =
+ addLabeledText(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.Description",
+ wDescription = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT));
+ lastControl =
+ addLabeledText(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.Company",
+ wCompany = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT));
+ lastControl =
+ addLabeledText(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.Department",
+ wDepartment = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT));
+ lastControl =
+ addLabeledText(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.Version",
+ wVersion = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT));
Label wlAutoExportMetadata = new Label(comp, SWT.RIGHT);
PropsUi.setLook(wlAutoExportMetadata);
@@ -388,7 +354,7 @@ public class ProjectDialog extends Dialog {
BaseMessages.getString(PKG,
"ProjectDialog.Label.AutoExportMetadata.Enable"));
FormData fdAutoExportMetadata = new FormData();
fdAutoExportMetadata.left = new FormAttachment(middle, margin);
- fdAutoExportMetadata.right = new FormAttachment(99, 0);
+ fdAutoExportMetadata.right = new FormAttachment(100, 0);
fdAutoExportMetadata.top = new FormAttachment(wlAutoExportMetadata, 0,
SWT.CENTER);
wAutoExportMetadata.setLayoutData(fdAutoExportMetadata);
wAutoExportMetadata.addListener(SWT.Selection, e ->
updateAutoExportMetadataWidgets());
@@ -407,48 +373,65 @@ public class ProjectDialog extends Dialog {
PropsUi.setLook(wAutoExportMetadataFilename);
FormData fdAutoExportMetadataFilename = new FormData();
fdAutoExportMetadataFilename.left = new FormAttachment(middle, margin);
- fdAutoExportMetadataFilename.right = new FormAttachment(99, 0);
+ fdAutoExportMetadataFilename.right = new FormAttachment(100, 0);
fdAutoExportMetadataFilename.top =
new FormAttachment(wlAutoExportMetadataFilename, 0, SWT.CENTER);
wAutoExportMetadataFilename.setLayoutData(fdAutoExportMetadataFilename);
- lastControl = wAutoExportMetadataFilename;
-
- Label wlUnitTestsBasePath = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlUnitTestsBasePath);
- wlUnitTestsBasePath.setText(
- BaseMessages.getString(PKG, "ProjectDialog.Label.UnitTestBaseFolder"));
- FormData fdlUnitTestsBasePath = new FormData();
- fdlUnitTestsBasePath.left = new FormAttachment(0, 0);
- fdlUnitTestsBasePath.right = new FormAttachment(middle, 0);
- fdlUnitTestsBasePath.top = new FormAttachment(lastControl, margin);
- wlUnitTestsBasePath.setLayoutData(fdlUnitTestsBasePath);
- wUnitTestsBasePath = new TextVar(variables, comp, SWT.SINGLE | SWT.BORDER
| SWT.LEFT);
- PropsUi.setLook(wUnitTestsBasePath);
- FormData fdUnitTestsBasePath = new FormData();
- fdUnitTestsBasePath.left = new FormAttachment(middle, margin);
- fdUnitTestsBasePath.right = new FormAttachment(99, 0);
- fdUnitTestsBasePath.top = new FormAttachment(wlUnitTestsBasePath, 0,
SWT.CENTER);
- wUnitTestsBasePath.setLayoutData(fdUnitTestsBasePath);
+ }
+
+ private Text addLabeledText(
+ Composite comp, int middle, int margin, Control lastControl, String
labelKey, Text widget) {
+ Label label = new Label(comp, SWT.RIGHT);
+ PropsUi.setLook(label);
+ label.setText(BaseMessages.getString(PKG, labelKey));
+ FormData fdl = new FormData();
+ fdl.left = new FormAttachment(0, 0);
+ fdl.right = new FormAttachment(middle, 0);
+ fdl.top = new FormAttachment(lastControl, margin);
+ label.setLayoutData(fdl);
+ PropsUi.setLook(widget);
+ FormData fd = new FormData();
+ fd.left = new FormAttachment(middle, margin);
+ fd.right = new FormAttachment(100, 0);
+ fd.top = new FormAttachment(label, 0, SWT.CENTER);
+ widget.setLayoutData(fd);
+ return widget;
+ }
+
+ private void createFoldersTab(CTabFolder folder, int margin) {
+ Composite comp = createTab(folder, "ProjectDialog.Tab.Folders");
+ int middle = props.getMiddlePct();
+ Control lastControl = null;
+
+ lastControl =
+ addLabeledTextVar(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.MetadataBaseFolder",
+ wMetadataBaseFolder = new TextVar(variables, comp, SWT.SINGLE |
SWT.BORDER | SWT.LEFT));
+ wMetadataBaseFolder.addModifyListener(e -> updateIVariables());
+
+ lastControl =
+ addLabeledTextVar(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.UnitTestBaseFolder",
+ wUnitTestsBasePath = new TextVar(variables, comp, SWT.SINGLE |
SWT.BORDER | SWT.LEFT));
wUnitTestsBasePath.addModifyListener(e -> updateIVariables());
- lastControl = wUnitTestsBasePath;
-
- Label wlDataSetCsvFolder = new Label(comp, SWT.RIGHT);
- PropsUi.setLook(wlDataSetCsvFolder);
- wlDataSetCsvFolder.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.DatasetCSVFolder"));
- FormData fdlDataSetCsvFolder = new FormData();
- fdlDataSetCsvFolder.left = new FormAttachment(0, 0);
- fdlDataSetCsvFolder.right = new FormAttachment(middle, 0);
- fdlDataSetCsvFolder.top = new FormAttachment(lastControl, margin);
- wlDataSetCsvFolder.setLayoutData(fdlDataSetCsvFolder);
- wDataSetCsvFolder = new TextVar(variables, comp, SWT.SINGLE | SWT.BORDER |
SWT.LEFT);
- PropsUi.setLook(wDataSetCsvFolder);
- FormData fdDataSetCsvFolder = new FormData();
- fdDataSetCsvFolder.left = new FormAttachment(middle, margin);
- fdDataSetCsvFolder.right = new FormAttachment(99, 0);
- fdDataSetCsvFolder.top = new FormAttachment(wlDataSetCsvFolder, 0,
SWT.CENTER);
- wDataSetCsvFolder.setLayoutData(fdDataSetCsvFolder);
+
+ lastControl =
+ addLabeledTextVar(
+ comp,
+ middle,
+ margin,
+ lastControl,
+ "ProjectDialog.Label.DatasetCSVFolder",
+ wDataSetCsvFolder = new TextVar(variables, comp, SWT.SINGLE |
SWT.BORDER | SWT.LEFT));
wDataSetCsvFolder.addModifyListener(e -> updateIVariables());
- lastControl = wDataSetCsvFolder;
Label wlEnforceHomeExecution = new Label(comp, SWT.RIGHT);
PropsUi.setLook(wlEnforceHomeExecution);
@@ -463,19 +446,138 @@ public class ProjectDialog extends Dialog {
PropsUi.setLook(wEnforceHomeExecution);
FormData fdEnforceHomeExecution = new FormData();
fdEnforceHomeExecution.left = new FormAttachment(middle, margin);
- fdEnforceHomeExecution.right = new FormAttachment(99, 0);
+ fdEnforceHomeExecution.right = new FormAttachment(100, 0);
fdEnforceHomeExecution.top = new FormAttachment(wlEnforceHomeExecution, 0,
SWT.CENTER);
wEnforceHomeExecution.setLayoutData(fdEnforceHomeExecution);
- lastControl = wlEnforceHomeExecution;
+ }
+
+ private TextVar addLabeledTextVar(
+ Composite comp,
+ int middle,
+ int margin,
+ Control lastControl,
+ String labelKey,
+ TextVar widget) {
+ Label label = new Label(comp, SWT.RIGHT);
+ PropsUi.setLook(label);
+ label.setText(BaseMessages.getString(PKG, labelKey));
+ FormData fdl = new FormData();
+ fdl.left = new FormAttachment(0, 0);
+ fdl.right = new FormAttachment(middle, 0);
+ if (lastControl == null) {
+ fdl.top = new FormAttachment(0, margin);
+ } else {
+ fdl.top = new FormAttachment(lastControl, margin);
+ }
+ label.setLayoutData(fdl);
+ PropsUi.setLook(widget);
+ FormData fd = new FormData();
+ fd.left = new FormAttachment(middle, margin);
+ fd.right = new FormAttachment(100, 0);
+ fd.top = new FormAttachment(label, 0, SWT.CENTER);
+ widget.setLayoutData(fd);
+ return widget;
+ }
+
+ private void createParentProjectTab(CTabFolder folder, int margin) {
+ Composite comp = createTab(folder, "ProjectDialog.Tab.ParentProject");
+ int middle = props.getMiddlePct();
+
+ Label wlParentProject = new Label(comp, SWT.RIGHT);
+ PropsUi.setLook(wlParentProject);
+ wlParentProject.setText(BaseMessages.getString(PKG,
"ProjectDialog.Label.ParentProject"));
+ FormData fdlParentProject = new FormData();
+ fdlParentProject.left = new FormAttachment(0, 0);
+ fdlParentProject.right = new FormAttachment(middle, 0);
+ fdlParentProject.top = new FormAttachment(0, margin);
+ wlParentProject.setLayoutData(fdlParentProject);
+ wParentProject = new ComboVar(variables, comp, SWT.SINGLE | SWT.BORDER |
SWT.LEFT);
+ PropsUi.setLook(wParentProject);
+ FormData fdParentProject = new FormData();
+ fdParentProject.left = new FormAttachment(middle, margin);
+ fdParentProject.right = new FormAttachment(100, 0);
+ fdParentProject.top = new FormAttachment(wlParentProject, 0, SWT.CENTER);
+ wParentProject.setLayoutData(fdParentProject);
+
+ Label wlParentFolders = new Label(comp, SWT.LEFT);
+ PropsUi.setLook(wlParentFolders);
+ wlParentFolders.setText(
+ BaseMessages.getString(PKG,
"ProjectDialog.Label.ParentProjectFolders"));
+ FormData fdlParentFolders = new FormData();
+ fdlParentFolders.left = new FormAttachment(0, 0);
+ fdlParentFolders.right = new FormAttachment(100, 0);
+ fdlParentFolders.top = new FormAttachment(wParentProject, 2 * margin);
+ wlParentFolders.setLayoutData(fdlParentFolders);
+
+ ColumnInfo[] columnInfo =
+ new ColumnInfo[] {
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Label.Folder"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Label.CopyOnce"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ YES_NO,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Label.CopyOnEnable"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ YES_NO,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Label.Overwrite"),
+ ColumnInfo.COLUMN_TYPE_CCOMBO,
+ YES_NO,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Label.ExclusionWildcard"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false,
+ false),
+ };
+ columnInfo[0].setUsingVariables(true);
+ columnInfo[0].setToolTip(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Tooltip.Folder"));
+ columnInfo[1].setToolTip(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Tooltip.CopyOnce"));
+ columnInfo[2].setToolTip(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Tooltip.CopyOnEnable"));
+ columnInfo[3].setToolTip(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Tooltip.Overwrite"));
+ columnInfo[4].setToolTip(
+ BaseMessages.getString(PKG,
"ProjectDialog.DetailTable.Tooltip.ExclusionWildcard"));
+
+ wParentFolders =
+ new TableView(
+ variables,
+ comp,
+ SWT.BORDER,
+ columnInfo,
+ Math.max(project.getParentProjectFolders().size(), 3),
+ e -> needingProjectRefresh = true,
+ props);
+ PropsUi.setLook(wParentFolders);
+ FormData fdParentFolders = new FormData();
+ fdParentFolders.left = new FormAttachment(0, 0);
+ fdParentFolders.right = new FormAttachment(100, 0);
+ fdParentFolders.top = new FormAttachment(wlParentFolders, margin);
+ fdParentFolders.bottom = new FormAttachment(100, 0);
+ wParentFolders.setLayoutData(fdParentFolders);
+ }
+
+ private void createVariablesTab(CTabFolder folder, int margin) {
+ Composite comp = createTab(folder, "ProjectDialog.Tab.Variables");
Label wlVariables = new Label(comp, SWT.LEFT);
PropsUi.setLook(wlVariables);
wlVariables.setText(
BaseMessages.getString(PKG,
"ProjectDialog.Group.Label.ProjectVariablesToSet"));
FormData fdlVariables = new FormData();
- fdlVariables.left = new FormAttachment(1, 0);
- fdlVariables.right = new FormAttachment(99, 0);
- fdlVariables.top = new FormAttachment(lastControl, 2 * margin);
+ fdlVariables.left = new FormAttachment(0, 0);
+ fdlVariables.right = new FormAttachment(100, 0);
+ fdlVariables.top = new FormAttachment(0, 0);
wlVariables.setLayoutData(fdlVariables);
ColumnInfo[] columnInfo =
@@ -506,40 +608,15 @@ public class ProjectDialog extends Dialog {
SWT.BORDER,
columnInfo,
Math.max(project.getDescribedVariables().size(), 3),
- null,
+ e -> needingProjectRefresh = true,
props);
PropsUi.setLook(wVariables);
FormData fdVariables = new FormData();
- fdVariables.left = new FormAttachment(1, 0);
- fdVariables.right = new FormAttachment(99, 0);
+ fdVariables.left = new FormAttachment(0, 0);
+ fdVariables.right = new FormAttachment(100, 0);
fdVariables.top = new FormAttachment(wlVariables, margin);
- fdVariables.bottom = new FormAttachment(100, -margin * 4);
- fdVariables.width = 300;
+ fdVariables.bottom = new FormAttachment(100, 0);
wVariables.setLayoutData(fdVariables);
- wVariables.addModifyListener(e -> needingProjectRefresh = true);
-
- // See if we need a project refresh/reload
- //
- wParentProject.addModifyListener(e -> needingProjectRefresh = true);
- wHome.addModifyListener(
- e -> {
- needingProjectRefresh = true;
- autoSetReadOnlyFromHome();
- });
-
- getData();
- updateReadOnlyWidgets();
- updateAutoExportMetadataWidgets();
-
- comp.pack();
- scroll.setContent(comp);
- scroll.setMinSize(comp.computeSize(SWT.DEFAULT, SWT.DEFAULT));
- shell.setMinimumSize(comp.getBounds().width, 200);
- shell.setDefaultButton(wOk);
- wName.setFocus();
- BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
-
- return returnValue;
}
/**
@@ -564,7 +641,6 @@ public class ProjectDialog extends Dialog {
private void updateReadOnlyWidgets() {
boolean editable = !wReadOnly.getSelection();
- // Config file browse is of limited use for archives; keep the relative
path editable.
wbConfigFile.setEnabled(editable);
wParentProject.setEnabled(editable);
wDescription.setEnabled(editable);
@@ -579,6 +655,17 @@ public class ProjectDialog extends Dialog {
wVariables.setEnabled(editable);
wVariables.setReadonly(!editable);
updateAutoExportMetadataWidgets();
+ updateParentFolderWidgets();
+ }
+
+ private void updateParentFolderWidgets() {
+ if (wParentFolders == null || wParentProject == null) {
+ return;
+ }
+ boolean editable =
+ !wReadOnly.getSelection() &&
StringUtils.isNotEmpty(wParentProject.getText());
+ wParentFolders.setEnabled(editable);
+ wParentFolders.setReadonly(!editable);
}
/** Filename is only meaningful when auto-export is enabled (and the project
is not read-only). */
@@ -590,8 +677,6 @@ public class ProjectDialog extends Dialog {
private void browseHomeFolder(Event event) {
String homeFolder = BaseDialog.presentDirectoryDialog(shell, wHome,
variables);
- // Set the name to the base folder if the name is empty
- //
try {
if (homeFolder != null && StringUtils.isEmpty(wName.getText())) {
FileObject file = HopVfs.getFileObject(homeFolder);
@@ -599,13 +684,11 @@ public class ProjectDialog extends Dialog {
}
} catch (Exception e) {
LogChannel.UI.logError("Error getting base filename of home folder: " +
homeFolder, e);
- // Don't change the name
}
}
private void browseConfigFolder(Event event) {
String configFileStr = null;
- // Set the root of the possible path to config file to project's root
String rootPath = wHome.getText();
File configFile =
@@ -637,8 +720,6 @@ public class ProjectDialog extends Dialog {
+ ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME;
}
- // Set the name to the base folder if the name is empty
- //
if (configFileStr != null) {
String relativeConfigFile = null;
if (!configFileStr.startsWith(rootPath)) {
@@ -648,7 +729,6 @@ public class ProjectDialog extends Dialog {
BaseMessages.getString(PKG,
"ProjectGuiPlugin.WrongConfigPath.Dialog.Message"));
box.open();
} else {
- // Calculate relative path to existing config file
String tmpConfigFile = StringUtils.difference(rootPath +
File.separator, configFileStr);
relativeConfigFile =
(tmpConfigFile.startsWith("/") ? tmpConfigFile.substring(1) :
tmpConfigFile);
@@ -675,9 +755,6 @@ public class ProjectDialog extends Dialog {
private void ok() {
try {
- // Do some extra validations to prevent bad data ending up in the
projects configuration
- //
-
String oriProjectName = projectConfig.getProjectName();
String oriProjectHome = projectConfig.getProjectHome();
@@ -689,7 +766,6 @@ public class ProjectDialog extends Dialog {
throw new HopException("Please specify a home folder for your
project");
}
- // Manage changing in project's home folder
if (projectHomeFolderChanged) {
MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
box.setText(BaseMessages.getString(PKG,
"ProjectDialog.ChangeHome.Dialog.Header"));
@@ -703,9 +779,6 @@ public class ProjectDialog extends Dialog {
}
}
- // If the home folder doesn't exist and project is new ask if it should
be created.
- // Never create folders for a read-only project (archives, HTTP, etc.).
- //
FileObject homeFolderObject =
HopVfs.getFileObject(variables.resolve(homeFolder));
if (!homeFolderObject.exists()) {
if (readOnly) {
@@ -724,7 +797,6 @@ public class ProjectDialog extends Dialog {
}
}
- // Renaming the project is not supported
String projectName = wName.getText();
if (StringUtils.isEmpty(projectName)) {
throw new HopException("Please give your new project a name");
@@ -738,8 +810,6 @@ public class ProjectDialog extends Dialog {
throw new HopException("Please specify project's configuration file
relative path!");
}
- // Read-only projects cannot create/write project-config.json: it must
already exist.
- //
if (readOnly) {
ProjectConfig verifyConfig =
new ProjectConfig(projectName, homeFolder, wConfigFile.getText());
@@ -762,7 +832,6 @@ public class ProjectDialog extends Dialog {
ProjectsConfig prjsCfg = ProjectsConfigSingleton.getConfig();
List<String> prjs = prjsCfg.listProjectConfigNames();
- // Check if project name is unique otherwise force the user to change it!
if (StringUtils.isEmpty(oriProjectName)
|| (StringUtils.isNotEmpty(oriProjectName) &&
!projectName.equals(oriProjectName))) {
for (String prj : prjs) {
@@ -795,7 +864,6 @@ public class ProjectDialog extends Dialog {
+ "' as parent project because we are going to create a
circular reference!");
}
- // Manage changing in project's home folder
if (this.editMode && !oriProjectName.equals(projectName)) {
MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO |
SWT.ICON_QUESTION);
box.setText(BaseMessages.getString(PKG,
"ProjectDialog.ChangeProjectName.Dialog.Header"));
@@ -811,7 +879,6 @@ public class ProjectDialog extends Dialog {
}
}
- // Change references to project's name if it changed
if (!oriProjectName.equals(projectName)) {
List<String> refs =
ProjectsUtil.getParentProjectReferences(oriProjectName);
@@ -876,8 +943,6 @@ public class ProjectDialog extends Dialog {
wVariables.setRowNums();
wVariables.optWidth(true);
- // Parent project...
- //
try {
wParentProject.setText(Const.NVL(project.getParentProjectName(), ""));
@@ -893,6 +958,19 @@ public class ProjectDialog extends Dialog {
BaseMessages.getString(PKG,
"ProjectDialog.ProjectList.Error.Dialog.Message"),
e);
}
+
+ List<ParentProjectFolder> parentFolders =
project.getParentProjectFolders();
+ for (int i = 0; i < parentFolders.size(); i++) {
+ ParentProjectFolder parentFolder = parentFolders.get(i);
+ TableItem item = wParentFolders.table.getItem(i);
+ item.setText(1, Const.NVL(parentFolder.getFolder(), ""));
+ item.setText(2, yesNo(parentFolder.isCopyOnce()));
+ item.setText(3, yesNo(parentFolder.isCopyOnEnable()));
+ item.setText(4, yesNo(parentFolder.isOverwrite()));
+ item.setText(5, Const.NVL(parentFolder.getExclusionWildcard(), ""));
+ }
+ wParentFolders.setRowNums();
+ wParentFolders.optWidth(true);
}
private void getInfo(Project project, ProjectConfig projectConfig) throws
HopException {
@@ -925,9 +1003,21 @@ public class ProjectDialog extends Dialog {
project.getDescribedVariables().add(variable);
}
- // Update the project to the right absolute configuration file (skip when
folder missing so user
- // can fix path)
- //
+ project.getParentProjectFolders().clear();
+ for (int i = 0; i < wParentFolders.nrNonEmpty(); i++) {
+ TableItem item = wParentFolders.getNonEmpty(i);
+ if (StringUtils.isEmpty(item.getText(1))) {
+ continue;
+ }
+ ParentProjectFolder parentFolder = new ParentProjectFolder();
+ parentFolder.setFolder(item.getText(1));
+ parentFolder.setCopyOnce(isYes(item.getText(2)));
+ parentFolder.setCopyOnEnable(isYes(item.getText(3)));
+ parentFolder.setOverwrite(isYes(item.getText(4)));
+ parentFolder.setExclusionWildcard(item.getText(5));
+ project.getParentProjectFolders().add(parentFolder);
+ }
+
if (StringUtils.isNotEmpty(projectConfig.getProjectHome())
&& StringUtils.isNotEmpty(projectConfig.configFilename)) {
try {
@@ -936,12 +1026,9 @@ public class ProjectDialog extends Dialog {
if (ProjectsGuiPlugin.extractMissingProjectPath(e) == null) {
throw new HopException(e);
}
- // Project folder does not exist yet; leave config filename unset so
user can edit path
}
}
- // Check for infinite loops (skip when parent chain has missing folder so
user can fix)
- //
try {
project.verifyProjectsChain(projectConfig.getProjectName(), variables);
} catch (Exception e) {
@@ -950,4 +1037,12 @@ public class ProjectDialog extends Dialog {
}
}
}
+
+ private static boolean isYes(String value) {
+ return "Y".equalsIgnoreCase(Const.NVL(value, ""));
+ }
+
+ private static String yesNo(boolean value) {
+ return value ? "Y" : "N";
+ }
}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/Defaults.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/Defaults.java
index 0abc0a7833..6726bcc326 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/Defaults.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/Defaults.java
@@ -28,4 +28,7 @@ public class Defaults {
* enabled and no custom filename is configured.
*/
public static final String DEFAULT_AUTO_EXPORT_METADATA_FILENAME =
"metadata.json";
+
+ /** Documentation page for project and environment properties. */
+ public static final String DOCUMENTATION_URI =
"projects/projects-environments.html";
}
diff --git
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
index 3c5a62a0b4..020cef141d 100644
---
a/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
+++
b/plugins/misc/projects/src/main/java/org/apache/hop/projects/util/ProjectsUtil.java
@@ -41,6 +41,7 @@ import org.apache.hop.metadata.util.HopMetadataUtil;
import org.apache.hop.projects.config.ProjectsConfig;
import org.apache.hop.projects.config.ProjectsConfigSingleton;
import org.apache.hop.projects.environment.LifecycleEnvironment;
+import org.apache.hop.projects.project.ParentProjectFolderSynchronizer;
import org.apache.hop.projects.project.Project;
import org.apache.hop.projects.project.ProjectConfig;
import org.apache.hop.ui.core.gui.HopNamespace;
@@ -143,6 +144,16 @@ public class ProjectsUtil {
//
HopNamespace.setNamespace(projectName);
+ // Copy configured parent-project folders into this project home. Do not
abort enabling the
+ // project when a template folder is missing or a file cannot be copied.
+ //
+ try {
+ ParentProjectFolderSynchronizer.synchronize(log, project, projectConfig,
variables);
+ } catch (Exception e) {
+ log.logError(
+ "Error synchronizing parent project folders for project '" +
projectName + "'", e);
+ }
+
// Save some history concerning the usage of the project
// but only in case Hop was started by HopGui because that is the only case
// where this info is valuable. Audit I/O must not block enabling a
project (e.g. Docker audit
diff --git
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
index cbe5492b94..90f4f1e8b0 100644
---
a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
+++
b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/project/messages/messages_en_US.properties
@@ -20,6 +20,21 @@
ProjectDialog.Button.Browse=Browse...
+ProjectDialog.Tab.Basic=Basic
+ProjectDialog.Tab.Folders=Folders
+ProjectDialog.Tab.ParentProject=Parent project
+ProjectDialog.Tab.Variables=Variables
+ProjectDialog.Label.ParentProjectFolders=Parent project folders to synchronize
+ProjectDialog.DetailTable.Label.Folder=Folder (relative to parent home)
+ProjectDialog.DetailTable.Label.CopyOnce=Copy once
+ProjectDialog.DetailTable.Label.CopyOnEnable=Copy on enable
+ProjectDialog.DetailTable.Label.Overwrite=Overwrite
+ProjectDialog.DetailTable.Label.ExclusionWildcard=Exclusion (regular
expression)
+ProjectDialog.DetailTable.Tooltip.Folder=Folder relative to the parent project
home. Use . for the parent root.
+ProjectDialog.DetailTable.Tooltip.CopyOnce=Copy when the destination folder is
missing or empty
+ProjectDialog.DetailTable.Tooltip.CopyOnEnable=Copy every time this project is
enabled
+ProjectDialog.DetailTable.Tooltip.Overwrite=Replace existing files in the
child project
+ProjectDialog.DetailTable.Tooltip.ExclusionWildcard=Java regular expression
matched against the file name and the path relative to the copied folder.
Example: .*\\.tmp
ProjectDialog.ChangeHome.Dialog.Header=Change project''s home?
ProjectDialog.ChangeHome.Dialog.Message=WARNING! Project''s HOME folder
changed from ''{0}'' to ''{1}''!\nThis change can result to unexpected results.
Are you sure you want to change it?
ProjectDialog.ChangeProjectName.Dialog.Header=Change project''s name?
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizerTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizerTest.java
new file mode 100644
index 0000000000..b61699f0ec
--- /dev/null
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ParentProjectFolderSynchronizerTest.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.projects.project;
+
+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.nio.file.Files;
+import java.nio.file.Path;
+import org.apache.hop.core.logging.HopLogStore;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.projects.config.ProjectsConfig;
+import org.apache.hop.projects.util.ProjectsUtil;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+class ParentProjectFolderSynchronizerTest {
+
+ @TempDir Path tempDir;
+
+ @BeforeAll
+ static void initLogging() {
+ HopLogStore.init();
+ }
+
+ @Test
+ void copyOnceIntoEmptyDestinationThenSkip() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "v1");
+ Files.createDirectories(child);
+
Files.writeString(child.resolve(ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME),
"{}");
+
+ Project project = projectWith(mapping("templates", true, false, false,
null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ Path copied = child.resolve("templates/core.hpl");
+ assertEquals("v1", Files.readString(copied));
+
+ Files.writeString(parent.resolve("templates/core.hpl"), "v2");
+ Files.writeString(parent.resolve("templates/extra.hpl"), "extra");
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("v1", Files.readString(copied));
+ assertFalse(Files.exists(child.resolve("templates/extra.hpl")));
+ }
+
+ @Test
+ void copyOnEnablePicksUpNewParentFiles() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "v1");
+ Files.createDirectories(child);
+
+ Project project = projectWith(mapping("templates", false, true, false,
null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+ assertEquals("v1", Files.readString(child.resolve("templates/core.hpl")));
+
+ Files.writeString(parent.resolve("templates/extra.hpl"), "extra");
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+ assertEquals("extra",
Files.readString(child.resolve("templates/extra.hpl")));
+ }
+
+ @Test
+ void overwriteFalseKeepsExistingFile() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "parent");
+ Files.createDirectories(child.resolve("templates"));
+ Files.writeString(child.resolve("templates/core.hpl"), "child");
+
+ Project project = projectWith(mapping("templates", false, true, false,
null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("child",
Files.readString(child.resolve("templates/core.hpl")));
+ }
+
+ @Test
+ void overwriteTrueReplacesExistingFile() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "parent");
+ Files.createDirectories(child.resolve("templates"));
+ Files.writeString(child.resolve("templates/core.hpl"), "child");
+
+ Project project = projectWith(mapping("templates", false, true, true,
null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("parent",
Files.readString(child.resolve("templates/core.hpl")));
+ }
+
+ @Test
+ void exclusionRegexSkipsBasenameAndRelativePath() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates/secret"));
+ Files.writeString(parent.resolve("templates/keep.hpl"), "keep");
+ Files.writeString(parent.resolve("templates/skip.tmp"), "tmp");
+ Files.writeString(parent.resolve("templates/secret/hidden.txt"), "secret");
+ Files.createDirectories(child);
+
+ Project project = projectWith(mapping("templates", false, true, true,
".*\\.tmp|secret/.*"));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("keep",
Files.readString(child.resolve("templates/keep.hpl")));
+ assertFalse(Files.exists(child.resolve("templates/skip.tmp")));
+ assertFalse(Files.exists(child.resolve("templates/secret/hidden.txt")));
+ }
+
+ @Test
+ void neverOverwritesChildProjectConfigOrGit() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve(".git"));
+ Files.writeString(parent.resolve(".git/config"), "parent-git");
+ Files.writeString(
+ parent.resolve(ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME),
"parent-config");
+ Files.writeString(parent.resolve("readme.txt"), "hello");
+ Files.createDirectories(child.resolve(".git"));
+ Files.writeString(child.resolve(".git/config"), "child-git");
+ Files.writeString(
+ child.resolve(ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME),
"child-config");
+
+ Project project = projectWith(mapping(".", false, true, true, null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("hello", Files.readString(child.resolve("readme.txt")));
+ assertEquals(
+ "child-config",
+
Files.readString(child.resolve(ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME)));
+ assertEquals("child-git", Files.readString(child.resolve(".git/config")));
+ }
+
+ @Test
+ void pathTraversalIsRejected() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Path escape = tempDir.resolve("escape");
+ Files.createDirectories(parent);
+ Files.createDirectories(child);
+ Files.createDirectories(escape);
+ Files.writeString(parent.resolve("safe.txt"), "parent");
+ Files.writeString(escape.resolve("marker.txt"), "untouched");
+
+ Project project = projectWith(mapping("../escape", false, true, true,
null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("untouched", Files.readString(escape.resolve("marker.txt")));
+ assertFalse(Files.exists(escape.resolve("safe.txt")));
+ }
+
+ @Test
+ void invalidExclusionRegexSkipsMapping() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "v1");
+ Files.createDirectories(child);
+
+ Project project = projectWith(mapping("templates", false, true, true,
"*.tmp"));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertFalse(Files.exists(child.resolve("templates/core.hpl")));
+ }
+
+ @Test
+ void readOnlyProjectIsNotCopiedInto() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("templates"));
+ Files.writeString(parent.resolve("templates/core.hpl"), "v1");
+ Files.createDirectories(child);
+
+ Project project = projectWith(mapping("templates", false, true, true,
null));
+ ProjectConfig config = childConfig(child);
+ config.setReadOnly(true);
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, config, variables(parent, child));
+
+ assertFalse(Files.exists(child.resolve("templates/core.hpl")));
+ }
+
+ @Test
+ void copyOnceOfParentRootIgnoresExistingProjectConfig() throws Exception {
+ Path parent = tempDir.resolve("parent");
+ Path child = tempDir.resolve("child");
+ Files.createDirectories(parent.resolve("pipelines"));
+ Files.writeString(parent.resolve("pipelines/start.hpl"), "start");
+ Files.createDirectories(child);
+
Files.writeString(child.resolve(ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME),
"{}");
+
+ Project project = projectWith(mapping(".", true, false, false, null));
+
+ ParentProjectFolderSynchronizer.synchronize(
+ LogChannel.GENERAL, project, childConfig(child), variables(parent,
child));
+
+ assertEquals("start",
Files.readString(child.resolve("pipelines/start.hpl")));
+ }
+
+ @Test
+ void matchesExclusionUsesBasenameAndRelativePath() {
+ java.util.regex.Pattern pattern =
java.util.regex.Pattern.compile(".*\\.tmp|secret/.*");
+ assertTrue(ParentProjectFolderSynchronizer.matchesExclusion(pattern,
"skip.tmp", "skip.tmp"));
+ assertTrue(
+ ParentProjectFolderSynchronizer.matchesExclusion(
+ pattern, "hidden.txt", "secret/hidden.txt"));
+ assertFalse(ParentProjectFolderSynchronizer.matchesExclusion(pattern,
"keep.hpl", "keep.hpl"));
+ }
+
+ private static Project projectWith(ParentProjectFolder mapping) {
+ Project project = new Project();
+ project.setParentProjectName("parent");
+ project.getParentProjectFolders().add(mapping);
+ return project;
+ }
+
+ private static ParentProjectFolder mapping(
+ String folder, boolean copyOnce, boolean copyOnEnable, boolean
overwrite, String exclusion) {
+ ParentProjectFolder mapping = new ParentProjectFolder();
+ mapping.setFolder(folder);
+ mapping.setCopyOnce(copyOnce);
+ mapping.setCopyOnEnable(copyOnEnable);
+ mapping.setOverwrite(overwrite);
+ mapping.setExclusionWildcard(exclusion);
+ return mapping;
+ }
+
+ private static ProjectConfig childConfig(Path child) {
+ return new ProjectConfig(
+ "child", child.toString(),
ProjectsConfig.DEFAULT_PROJECT_CONFIG_FILENAME);
+ }
+
+ private static IVariables variables(Path parent, Path child) {
+ IVariables variables = new Variables();
+ variables.setVariable(ProjectsUtil.VARIABLE_PROJECT_HOME,
child.toString());
+ variables.setVariable(ProjectsUtil.VARIABLE_PARENT_PROJECT_HOME,
parent.toString());
+ return variables;
+ }
+}
diff --git
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ProjectTest.java
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ProjectTest.java
index bdcdc1003e..db7fa3289e 100644
---
a/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ProjectTest.java
+++
b/plugins/misc/projects/src/test/java/org/apache/hop/projects/project/ProjectTest.java
@@ -200,6 +200,37 @@ public class ProjectTest {
assertEquals(childHome.toString(),
variables.getVariable(ProjectsUtil.VARIABLE_PROJECT_HOME));
}
+ @Test
+ public void testParentProjectFoldersSerialization() throws Exception {
+ File tempFile = Files.createTempFile("project-config-parent-folders",
".json").toFile();
+ tempFile.deleteOnExit();
+
+ try {
+ Project project = new Project(tempFile.getAbsolutePath());
+ ParentProjectFolder folder = new ParentProjectFolder();
+ folder.setFolder("templates");
+ folder.setCopyOnce(true);
+ folder.setCopyOnEnable(true);
+ folder.setOverwrite(false);
+ folder.setExclusionWildcard(".*\\.tmp");
+ project.getParentProjectFolders().add(folder);
+ project.saveToFile();
+
+ Project readProject = new Project(tempFile.getAbsolutePath());
+ readProject.readFromFile();
+
+ assertEquals(1, readProject.getParentProjectFolders().size());
+ ParentProjectFolder read = readProject.getParentProjectFolders().get(0);
+ assertEquals("templates", read.getFolder());
+ assertTrue(read.isCopyOnce());
+ assertTrue(read.isCopyOnEnable());
+ assertFalse(read.isOverwrite());
+ assertEquals(".*\\.tmp", read.getExclusionWildcard());
+ } finally {
+ tempFile.delete();
+ }
+ }
+
@Test
public void testIsArchiveUri() {
assertTrue(ProjectConfig.isArchiveUri("zip:file:///tmp/project.zip!/my-project"));