diff --git a/.gitignore b/.gitignore index 64433a06aaa..28e2f00e632 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ screenshots/ # Docker script leaving audit data docker/local-data/ /docker/marketplace-nexus/.env + +# Jenkins-style local deploy output (verify-ci-snapshot-zips.sh) +local-snapshots-dir/ diff --git a/Jenkinsfile b/Jenkinsfile index d604d787c08..406bc894975 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -113,12 +113,23 @@ pipeline { } } } + stage('Assembly size check') { + when { + anyOf { changeset pattern: "^(?!docs).*^(?!integration-tests).*" , comparator: "REGEXP" ; equals expected: true, actual: params.FORCE_BUILD } + } + steps { + // ASF Artifactory rejects packages over ~850MB; fail early with margin. + sh "./tools/check-assembly-size.sh" + } + } stage('Unzip Apache Hop'){ when { anyOf { changeset pattern: "^(?!docs).*^(?!integration-tests).*" , comparator: "REGEXP" ; equals expected: true, actual: params.FORCE_BUILD } } steps{ sh "unzip ./assemblies/client/target/hop-client-*.zip -d ./assemblies/client/target/" + // Optional Wave 1 plugins are marketplace-only; install into the extracted client for IT/Docker + sh "./tools/install-wave1-plugins.sh ./assemblies/client/target/hop" sh "unzip ./assemblies/web/target/hop.war -d ./assemblies/web/target/webapp" } } diff --git a/assemblies/client/pom.xml b/assemblies/client/pom.xml index c7e1dd1c21e..155ae1ef27d 100644 --- a/assemblies/client/pom.xml +++ b/assemblies/client/pom.xml @@ -61,18 +61,17 @@ ${project.version} zip + org.apache.hop hop-engines-beam ${project.version} + libs zip - - org.apache.hop - hop-engines-spark - ${project.version} - zip - + org.apache.hop hop-ui @@ -144,4 +143,25 @@ + + + + beam + + + org.apache.hop + hop-engines-beam + ${project.version} + zip + + + + + diff --git a/assemblies/client/src/assembly/assembly.xml b/assemblies/client/src/assembly/assembly.xml index d25f356db3c..58b15cad028 100644 --- a/assemblies/client/src/assembly/assembly.xml +++ b/assemblies/client/src/assembly/assembly.xml @@ -40,6 +40,9 @@ org.apache.hop:hop-core:zip org.apache.hop:hop-engine:zip org.apache.hop:hop-ui:zip + + org.apache.hop:hop-engines-beam:zip:libs + org.apache.hop:hop-engines-beam:zip org.apache.hop:hop-ui-rcp:zip diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml index 30d0e6d0f52..36486762b9c 100644 --- a/assemblies/plugins/pom.xml +++ b/assemblies/plugins/pom.xml @@ -618,12 +618,7 @@ ${project.version} zip - - org.apache.hop - hop-engines-spark - ${project.version} - zip - + org.apache.hop hop-misc-async @@ -636,27 +631,18 @@ ${project.version} zip - - org.apache.hop - hop-misc-documentation - ${project.version} - zip - + org.apache.hop hop-misc-git ${project.version} zip + + org.apache.hop - hop-misc-import - ${project.version} - zip - - - org.apache.hop - hop-misc-mail + hop-misc-marketplace ${project.version} zip @@ -672,18 +658,14 @@ ${project.version} zip - - org.apache.hop - hop-misc-py4j - 2.19.0-SNAPSHOT - zip - + org.apache.hop hop-misc-reflection ${project.version} zip + org.apache.hop hop-misc-rest @@ -708,97 +690,20 @@ 2.19.0-SNAPSHOT zip - - - org.apache.hop - hop-tech-arrow - ${project.version} - zip - - - org.apache.hop - hop-tech-avro - ${project.version} - zip - - - org.apache.hop - hop-tech-aws - ${project.version} - zip - - - org.apache.hop - hop-tech-azure - ${project.version} - zip - - - org.apache.hop - hop-tech-cassandra - ${project.version} - zip - - - org.apache.hop - hop-tech-databricks - ${project.version} - zip - - - org.apache.hop - hop-tech-dropbox - ${project.version} - zip - - - org.apache.hop - hop-tech-elastic - ${project.version} - zip - - - org.apache.hop - hop-tech-google - ${project.version} - zip - - - org.apache.hop - hop-tech-minio - ${project.version} - zip - - - org.apache.hop - hop-tech-mongodb - ${project.version} - zip - - - org.apache.hop - hop-tech-neo4j - ${project.version} - zip - - - org.apache.hop - hop-tech-opensearch - ${project.version} - zip - - - org.apache.hop - hop-tech-parquet - ${project.version} - zip - - - org.apache.hop - hop-tech-salesforce - ${project.version} - zip - + + + + + + + + + + + + + + org.apache.hop hop-tech-vault @@ -1009,24 +914,14 @@ ${project.version} zip - - org.apache.hop - hop-transform-drools - ${project.version} - zip - + org.apache.hop hop-transform-dynamicsqlrow ${project.version} zip - - org.apache.hop - hop-transform-edi2xml - ${project.version} - zip - + org.apache.hop hop-transform-excel @@ -1051,12 +946,7 @@ ${project.version} zip - - org.apache.hop - hop-transform-fake - ${project.version} - zip - + org.apache.hop hop-transform-fieldschangesequence @@ -1417,12 +1307,7 @@ ${project.version} zip - - org.apache.hop - hop-transform-script - ${project.version} - zip - + org.apache.hop hop-transform-selectvalues @@ -1507,12 +1392,7 @@ ${project.version} zip - - org.apache.hop - hop-transform-stanfordnlp - ${project.version} - zip - + org.apache.hop hop-transform-streamlookup @@ -1591,12 +1471,7 @@ ${project.version} zip - - org.apache.hop - hop-transform-tika - ${project.version} - zip - + org.apache.hop hop-transform-tokenreplacement @@ -1633,12 +1508,7 @@ ${project.version} zip - - org.apache.hop - hop-transform-vcard - ${project.version} - zip - + org.apache.hop hop-transform-webserviceavailable diff --git a/assemblies/plugins/src/assembly/assembly.xml b/assemblies/plugins/src/assembly/assembly.xml index ed458154b5b..199b54fa4c5 100644 --- a/assemblies/plugins/src/assembly/assembly.xml +++ b/assemblies/plugins/src/assembly/assembly.xml @@ -35,24 +35,35 @@ false + databases **/*.zip + + **/target/** + transforms **/*.zip + + **/target/** + actions **/*.zip + + **/target/** + \ No newline at end of file diff --git a/assemblies/static/src/main/resources/hop b/assemblies/static/src/main/resources/hop index 567efb609fe..ed52c6936f3 100755 --- a/assemblies/static/src/main/resources/hop +++ b/assemblies/static/src/main/resources/hop @@ -86,16 +86,16 @@ Linux) export GDK_BACKEND=x11 fi if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -108,6 +108,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi + "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.hop.Hop "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-conf.bat b/assemblies/static/src/main/resources/hop-conf.bat index e33f891e3ec..12726246c83 100644 --- a/assemblies/static/src/main/resources/hop-conf.bat +++ b/assemblies/static/src/main/resources/hop-conf.bat @@ -1,105 +1,113 @@ -@echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal - -REM switch to script directory -cd /D %~dp0 - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Conf -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-conf.bat]=================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% -echo. -echo ===[Starting HopConfig]========================================================= - -%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% +@echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal + +REM switch to script directory +cd /D %~dp0 + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Conf +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-conf.bat]=================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% +echo. +echo ===[Starting HopConfig]========================================================= + +%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% diff --git a/assemblies/static/src/main/resources/hop-conf.sh b/assemblies/static/src/main/resources/hop-conf.sh index b279b2ad933..67dd91794e5 100755 --- a/assemblies/static/src/main/resources/hop-conf.sh +++ b/assemblies/static/src/main/resources/hop-conf.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -103,6 +103,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.config.HopConfig "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-encrypt.bat b/assemblies/static/src/main/resources/hop-encrypt.bat index a5877064c0d..66b37dea1e4 100644 --- a/assemblies/static/src/main/resources/hop-encrypt.bat +++ b/assemblies/static/src/main/resources/hop-encrypt.bat @@ -1,109 +1,117 @@ -echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -echo off -setlocal - -REM switch to script directory -cd /D %~dp0 - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx64m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - - -echo ===[Environment Settings - hop-encrypt.bat]==================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.core.encryption.Encr %_cmdline% -echo. -echo ===[Starting HopEncrypt]========================================================= - -%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.encryption.HopEncrypt %_cmdline% -@echo off +echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +echo off +setlocal + +REM switch to script directory +cd /D %~dp0 + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx64m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + + +echo ===[Environment Settings - hop-encrypt.bat]==================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.core.encryption.Encr %_cmdline% +echo. +echo ===[Starting HopEncrypt]========================================================= + +%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.encryption.HopEncrypt %_cmdline% +@echo off :End \ No newline at end of file diff --git a/assemblies/static/src/main/resources/hop-encrypt.sh b/assemblies/static/src/main/resources/hop-encrypt.sh index b89a53adedf..365b2e72d65 100755 --- a/assemblies/static/src/main/resources/hop-encrypt.sh +++ b/assemblies/static/src/main/resources/hop-encrypt.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -103,6 +103,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -classpath "${CLASSPATH}" org.apache.hop.encryption.HopEncrypt "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-gui-nolog.bat b/assemblies/static/src/main/resources/hop-gui-nolog.bat index b830fecc0f5..042384cd8ef 100644 --- a/assemblies/static/src/main/resources/hop-gui-nolog.bat +++ b/assemblies/static/src/main/resources/hop-gui-nolog.bat @@ -24,8 +24,16 @@ cd /D %~dp0 REM Option to change the Characterset of the Windows Shell to show foreign caracters if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\* +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\swt\win64\* + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) set _temphelp=0 if [%1]==[help] set _temphelp=1 diff --git a/assemblies/static/src/main/resources/hop-gui.bat b/assemblies/static/src/main/resources/hop-gui.bat index e6ef8c93e7f..7fb165def5c 100755 --- a/assemblies/static/src/main/resources/hop-gui.bat +++ b/assemblies/static/src/main/resources/hop-gui.bat @@ -1,125 +1,133 @@ -@echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal - -REM switch to script directory -cd /D %~dp0 - -REM Option to change the Characterset of the Windows Shell to show foreign caracters -if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -set _temphelp=0 -if [%1]==[help] set _temphelp=1 -if [%1]==[Help] set _temphelp=1 -if %_temphelp%==1 (GOTO Help) ELSE (GOTO NormalStart) - -:Help -echo ===[Hop Help - hop-gui.bat]=============================================== -echo Starts Hop GUI and keeps this console window open so you can see log output. -echo To start without a console window, use hop-gui-nolog.bat instead. -echo. -echo Normally, no parameters are required to start Hop. There is a debug mode -echo that you can start by passing in DEBUG as the first parameter after hop-gui.bat -echo. -echo Example: -echo hop-gui.bat DEBUG -echo -echo The debug mode opens port 5005 locally when Hop starts allowing you to attaching -echo a debugger from your favorite Java IDE tool and step code. -echo ========================================================================== -GOTO End - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5005 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger + turning on GUI debug logging -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -DHOP_LOG_LEVEL=Debug) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-gui.bat]=================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.ui.hopgui.HopGui -echo. -echo ===[Starting Hop]========================================================= - -%_HOP_JAVA% -classpath %CLASSPATH% -Dswt.autoScale=false -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.ui.hopgui.HopGui -if ERRORLEVEL 1 (pause) - -:End -endlocal +@echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal + +REM switch to script directory +cd /D %~dp0 + +REM Option to change the Characterset of the Windows Shell to show foreign caracters +if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +set _temphelp=0 +if [%1]==[help] set _temphelp=1 +if [%1]==[Help] set _temphelp=1 +if %_temphelp%==1 (GOTO Help) ELSE (GOTO NormalStart) + +:Help +echo ===[Hop Help - hop-gui.bat]=============================================== +echo Starts Hop GUI and keeps this console window open so you can see log output. +echo To start without a console window, use hop-gui-nolog.bat instead. +echo. +echo Normally, no parameters are required to start Hop. There is a debug mode +echo that you can start by passing in DEBUG as the first parameter after hop-gui.bat +echo. +echo Example: +echo hop-gui.bat DEBUG +echo +echo The debug mode opens port 5005 locally when Hop starts allowing you to attaching +echo a debugger from your favorite Java IDE tool and step code. +echo ========================================================================== +GOTO End + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5005 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger + turning on GUI debug logging +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -DHOP_LOG_LEVEL=Debug) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=GUI +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-gui.bat]=================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.ui.hopgui.HopGui +echo. +echo ===[Starting Hop]========================================================= + +%_HOP_JAVA% -classpath %CLASSPATH% -Dswt.autoScale=false -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.ui.hopgui.HopGui +if ERRORLEVEL 1 (pause) + +:End +endlocal diff --git a/assemblies/static/src/main/resources/hop-gui.sh b/assemblies/static/src/main/resources/hop-gui.sh index ec2dbc546f1..188ec8af383 100755 --- a/assemblies/static/src/main/resources/hop-gui.sh +++ b/assemblies/static/src/main/resources/hop-gui.sh @@ -111,7 +111,7 @@ Darwin) HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; esac -CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/$os_path/$arch_path/*" +CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/$os_path/$arch_path/*" @@ -122,6 +122,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.ui.hopgui.HopGui "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-import.bat b/assemblies/static/src/main/resources/hop-import.bat index 582dcdb396f..5a4b91e88c8 100644 --- a/assemblies/static/src/main/resources/hop-import.bat +++ b/assemblies/static/src/main/resources/hop-import.bat @@ -1,108 +1,116 @@ -echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -echo off -setlocal - -REM switch to script directory -cd /D %~dp0 - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Import -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-import.bat]=================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% -echo. -echo ===[Starting HopConfig]========================================================= - -%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.imp.HopImport %_cmdline% -@echo off -:End +echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +echo off +setlocal + +REM switch to script directory +cd /D %~dp0 + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Import +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-import.bat]=================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% +echo. +echo ===[Starting HopConfig]========================================================= + +%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.imp.HopImport %_cmdline% +@echo off +:End diff --git a/assemblies/static/src/main/resources/hop-import.sh b/assemblies/static/src/main/resources/hop-import.sh index 5669e164e5e..5ee373c1ae9 100755 --- a/assemblies/static/src/main/resources/hop-import.sh +++ b/assemblies/static/src/main/resources/hop-import.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -103,6 +103,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.imp.HopImport "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-run.bat b/assemblies/static/src/main/resources/hop-run.bat index 424a2737daa..9c4e2152278 100755 --- a/assemblies/static/src/main/resources/hop-run.bat +++ b/assemblies/static/src/main/resources/hop-run.bat @@ -1,128 +1,136 @@ -@echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal - -REM switch to script directory -cd /D %~dp0 - -REM Option to change the Characterset of the Windows Shell to show foreign caracters -if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -set _temphelp=0 -if [%1]==[help] set _temphelp=1 -if [%1]==[Help] set _temphelp=1 -if %_temphelp%==1 (GOTO Help) ELSE (GOTO NormalStart) - -:Help -echo ===[HopRun - hop-run.bat]================================================= -echo. -echo Example: -echo hop-run.bat --file=C:\Users\usbra\Desktop\converted_financial_metrics\update_dashboard_financials.hwf --environment=converted_financial_metrics --project=converted_financial_metrics --runconfig=local -echo -echo The above gives a full path to a file. It assumes a project has been created for that file called 'converted_financial_metrics' -echo The project also has an environment called 'converted_financial_metrics', that was created when creating the project. -echo Finally, 'local' is the name of the local configuration json file present in the project's metadata folder. There is one for workflows and another for pipelines. -echo ========================================================================== -GOTO End - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA="java" -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m" - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5005 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-run.bat]=================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo. -echo Consolidated parameters to pass to HopRun are -echo %_cmdline%% -echo. -echo Command to start HopRun will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.run.HopRun %_cmdline%% -echo. -echo ===[Starting HopRun]========================================================= +@echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal + +REM switch to script directory +cd /D %~dp0 + +REM Option to change the Characterset of the Windows Shell to show foreign caracters +if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +set _temphelp=0 +if [%1]==[help] set _temphelp=1 +if [%1]==[Help] set _temphelp=1 +if %_temphelp%==1 (GOTO Help) ELSE (GOTO NormalStart) + +:Help +echo ===[HopRun - hop-run.bat]================================================= +echo. +echo Example: +echo hop-run.bat --file=C:\Users\usbra\Desktop\converted_financial_metrics\update_dashboard_financials.hwf --environment=converted_financial_metrics --project=converted_financial_metrics --runconfig=local +echo +echo The above gives a full path to a file. It assumes a project has been created for that file called 'converted_financial_metrics' +echo The project also has an environment called 'converted_financial_metrics', that was created when creating the project. +echo Finally, 'local' is the name of the local configuration json file present in the project's metadata folder. There is one for workflows and another for pipelines. +echo ========================================================================== +GOTO End + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA="java" +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m" + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5005 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-run.bat]=================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo. +echo Consolidated parameters to pass to HopRun are +echo %_cmdline%% +echo. +echo Command to start HopRun will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.run.HopRun %_cmdline%% +echo. +echo ===[Starting HopRun]========================================================= %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.run.HopRun %_cmdline%% \ No newline at end of file diff --git a/assemblies/static/src/main/resources/hop-run.sh b/assemblies/static/src/main/resources/hop-run.sh index f25c19bac80..83c03b7f5f4 100755 --- a/assemblies/static/src/main/resources/hop-run.sh +++ b/assemblies/static/src/main/resources/hop-run.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -103,6 +103,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.run.HopRun "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-search.bat b/assemblies/static/src/main/resources/hop-search.bat index be6d8e37a5d..35571bb4640 100644 --- a/assemblies/static/src/main/resources/hop-search.bat +++ b/assemblies/static/src/main/resources/hop-search.bat @@ -1,105 +1,113 @@ -@echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal - -REM switch to script directory -cd /D %~dp0 - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Search -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-search.bat]=================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% -echo. -echo ===[Starting HopConfig]========================================================= - -%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.search.HopSearch %_cmdline% +@echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal + +REM switch to script directory +cd /D %~dp0 + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Search +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-search.bat]=================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.config.HopConfig %_cmdline% +echo. +echo ===[Starting HopConfig]========================================================= + +%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.search.HopSearch %_cmdline% diff --git a/assemblies/static/src/main/resources/hop-search.sh b/assemblies/static/src/main/resources/hop-search.sh index 6bba102dea0..607f53261e0 100755 --- a/assemblies/static/src/main/resources/hop-search.sh +++ b/assemblies/static/src/main/resources/hop-search.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -103,6 +103,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.search.HopSearch "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop-server.bat b/assemblies/static/src/main/resources/hop-server.bat index 40227e5c65e..d47d761bf04 100644 --- a/assemblies/static/src/main/resources/hop-server.bat +++ b/assemblies/static/src/main/resources/hop-server.bat @@ -1,109 +1,117 @@ -@echo off -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal - -REM switch to script directory -cd /D %~dp0 - -REM Option to change the Characterset of the Windows Shell to show foreign caracters -if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA=java -) - -REM # Settings for all OSses - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m - -REM -REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 -REM to allow attaching a debugger to step code. -if [%1]==[DEBUG] ( -REM # optional line for attaching a debugger -set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) - -REM Pass HOP variables if they're set. -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Server -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -echo ===[Environment Settings - hop-server.bat]==================================== -echo. -echo Java identified as %_HOP_JAVA% -echo. -echo HOP_OPTIONS=%HOP_OPTIONS% -echo. -echo. -rem ===[Collect command line arguments...]====================================== -if [%1]==[DEBUG] ( -FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y -GOTO Run -) -set _cmdline=%* - -:Run -echo Command to start Hop will be: -echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.www.HopServer %_cmdline% -echo. -echo ===[Starting HopServer]========================================================= - +@echo off +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal + +REM switch to script directory +cd /D %~dp0 + +REM Option to change the Characterset of the Windows Shell to show foreign caracters +if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA=java +) + +REM # Settings for all OSses + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS=-Xmx2048m + +REM +REM If the user passes in DEBUG as the first parameter, it starts Hop in debugger mode and opens port 5009 +REM to allow attaching a debugger to step code. +if [%1]==[DEBUG] ( +REM # optional line for attaching a debugger +set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5009) + +REM Pass HOP variables if they're set. +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Server +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +echo ===[Environment Settings - hop-server.bat]==================================== +echo. +echo Java identified as %_HOP_JAVA% +echo. +echo HOP_OPTIONS=%HOP_OPTIONS% +echo. +echo. +rem ===[Collect command line arguments...]====================================== +if [%1]==[DEBUG] ( +FOR /f "tokens=1*" %%x IN ("%*") DO set _cmdline=%%y +GOTO Run +) +set _cmdline=%* + +:Run +echo Command to start Hop will be: +echo %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.www.HopServer %_cmdline% +echo. +echo ===[Starting HopServer]========================================================= + %_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.www.HopServer %_cmdline% \ No newline at end of file diff --git a/assemblies/static/src/main/resources/hop-server.sh b/assemblies/static/src/main/resources/hop-server.sh index 06cedd3d4a7..d732505d4d8 100755 --- a/assemblies/static/src/main/resources/hop-server.sh +++ b/assemblies/static/src/main/resources/hop-server.sh @@ -81,16 +81,16 @@ Linux) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/linux/$(uname -m)/*" fi ;; Darwin) if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch = aarch64"; then - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/arm64/*" else - CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*" + CLASSPATH="lib/core/*:lib/spark-client/*:lib/swt/osx/x86_64/*" fi HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread" ;; @@ -108,6 +108,17 @@ if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d "lib/spark-clients/${HOP_SPARK CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g') CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*" fi + +# Beam SDKs/runners live with the optional Beam plugin (not lib/beam). +# Only on the system classpath when marketplace (or -Pbeam) has installed them. +if [ -d "plugins/engines/beam/lib-beam" ]; then + CLASSPATH="${CLASSPATH}:plugins/engines/beam/lib-beam/*" +fi +if [ -d "plugins/engines/beam" ]; then + # Hop Beam plugin jars at the plugin root (exclude subdirs via non-recursive glob is fine for /*.jar intent; + # shell * includes jars and may pick non-jars; match historical lib/* style) + CLASSPATH="${CLASSPATH}:plugins/engines/beam/*" +fi "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath "${CLASSPATH}" org.apache.hop.www.HopServer "$@" EXITCODE=$? diff --git a/assemblies/static/src/main/resources/hop.bat b/assemblies/static/src/main/resources/hop.bat index 635a87f575b..0b624c33644 100755 --- a/assemblies/static/src/main/resources/hop.bat +++ b/assemblies/static/src/main/resources/hop.bat @@ -1,104 +1,112 @@ -@echo off - -REM -REM Licensed to the Apache Software Foundation (ASF) under one or more -REM contributor license agreements. See the NOTICE file distributed with -REM this work for additional information regarding copyright ownership. -REM The ASF licenses this file to You under the Apache License, Version 2.0 -REM (the "License"); you may not use this file except in compliance with -REM the License. You may obtain a copy of the License at -REM -REM http://www.apache.org/licenses/LICENSE-2.0 -REM -REM Unless required by applicable law or agreed to in writing, software -REM distributed under the License is distributed on an "AS IS" BASIS, -REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -REM See the License for the specific language governing permissions and -REM limitations under the License. -REM - -setlocal EnableDelayedExpansion - -REM switch to script directory -cd /D %~dp0 - -REM Option to change the character set of the Windows Shell to show foreign characters -if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% - -set LIBSPATH=lib\core;lib\beam -set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\* - -REM Optional versioned Spark client pack -if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( - set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* -) - - - - -:NormalStart -REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java -REM -if not "%HOP_JAVA_HOME%"=="" ( - set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" -) else if not "%JAVA_HOME%"=="" ( - set _HOP_JAVA="%JAVA_HOME%\bin\java" -) else ( - set _HOP_JAVA="java" -) - -REM # Settings for all OSses -REM - -if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m" - -REM See if we need to enable some remote debugging options for our developers. -REM -FOR %%a in (%*) DO ( - if "%%~a" == "--dev-debug" ( - set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5010 - ) - if "%%~a" == "--dev-debug-wait" ( - set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5010 - ) -) - -REM Pass HOP variables if they're set. -REM -if not "%HOP_AUDIT_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" -) else ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit -) -if not "%HOP_CONFIG_FOLDER%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" -) -if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" -) -if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" -) -if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% -) -if not "%HOP_AES_ENCODER_KEY%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% -) -if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( - set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% -) - -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run -set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y -set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED - -REM ===[Collect command line arguments...]====================================== -REM -set _cmdline=%* - -:Run - -%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.hop.Hop %_cmdline%% - +@echo off + +REM +REM Licensed to the Apache Software Foundation (ASF) under one or more +REM contributor license agreements. See the NOTICE file distributed with +REM this work for additional information regarding copyright ownership. +REM The ASF licenses this file to You under the Apache License, Version 2.0 +REM (the "License"); you may not use this file except in compliance with +REM the License. You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM + +setlocal EnableDelayedExpansion + +REM switch to script directory +cd /D %~dp0 + +REM Option to change the character set of the Windows Shell to show foreign characters +if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING% + +set LIBSPATH=lib\core +set CLASSPATH=lib\core\*;lib\spark-client\*;lib\swt\win64\* + +REM Optional versioned Spark client pack +if defined HOP_SPARK_CLIENT_VERSION if exist "lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" ( + set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\* +) + +REM Beam SDKs under optional plugin (plugins\engines\beam\lib-beam) +if exist "plugins\engines\beam\lib-beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\lib-beam\* +) +if exist "plugins\engines\beam\" ( + set CLASSPATH=%CLASSPATH%;plugins\engines\beam\* +) + + + + +:NormalStart +REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java +REM +if not "%HOP_JAVA_HOME%"=="" ( + set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java" +) else if not "%JAVA_HOME%"=="" ( + set _HOP_JAVA="%JAVA_HOME%\bin\java" +) else ( + set _HOP_JAVA="java" +) + +REM # Settings for all OSses +REM + +if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m" + +REM See if we need to enable some remote debugging options for our developers. +REM +FOR %%a in (%*) DO ( + if "%%~a" == "--dev-debug" ( + set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5010 + ) + if "%%~a" == "--dev-debug-wait" ( + set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5010 + ) +) + +REM Pass HOP variables if they're set. +REM +if not "%HOP_AUDIT_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%" +) else ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit +) +if not "%HOP_CONFIG_FOLDER%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%" +) +if not "%HOP_SHARED_JDBC_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%" +) +if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%" +) +if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN% +) +if not "%HOP_AES_ENCODER_KEY%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY% +) +if not "%HOP_AES_ENCODER_KEY_FILE%"=="" ( + set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE% +) + +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run +set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y +set HOP_OPTIONS=%HOP_OPTIONS% --add-opens java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.invoke=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.nio.cs=ALL-UNNAMED --add-opens java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.security.jgss/sun.security.krb5=ALL-UNNAMED --add-exports java.base/sun.nio.ch=ALL-UNNAMED + +REM ===[Collect command line arguments...]====================================== +REM +set _cmdline=%* + +:Run + +%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% %HOP_OPTIONS% org.apache.hop.hop.Hop %_cmdline%% + diff --git a/core/src/main/java/org/apache/hop/core/AttributesContext.java b/core/src/main/java/org/apache/hop/core/AttributesContext.java new file mode 100644 index 00000000000..c0d8dcc0057 --- /dev/null +++ b/core/src/main/java/org/apache/hop/core/AttributesContext.java @@ -0,0 +1,145 @@ +/* + * 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.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +/** + * Generic carrier for cross-plugin extension points. + * + *

Implements {@link IAttributes} so optional plugins can exchange namespaced group/key/value + * data without depending on each other. Typical groups: {@code marketplace}, resource checks + * ({@code resources}), etc. + * + *

Optional identity fields ({@link #projectName}, {@link #environmentName}, {@link #purpose}, + * {@link #projectHome}, {@link #configurationFiles}) describe the surrounding project/lifecycle + * context when that is what is being activated; they are not required for every use of this class. + */ +@Getter +@Setter +public class AttributesContext implements IAttributes { + + private String projectName; + private String environmentName; + private String purpose; + private String projectHome; + private List configurationFiles = new ArrayList<>(); + + private Map> attributesMap = new HashMap<>(); + + public AttributesContext() { + // empty + } + + /** Deep-copy attributes from any {@link IAttributes} source. */ + public AttributesContext(IAttributes source) { + copyAttributesFrom(source); + } + + /** + * Replace this context's attributes map with a deep copy of {@code source}. Does not clear + * identity fields. + */ + public void copyAttributesFrom(IAttributes source) { + attributesMap = deepCopy(source != null ? source.getAttributesMap() : null); + } + + /** Copy this context's attributes into {@code target} (deep copy). */ + public void copyAttributesTo(IAttributes target) { + if (target == null) { + return; + } + target.setAttributesMap(deepCopy(attributesMap)); + } + + private static Map> deepCopy( + Map> source) { + Map> copy = new HashMap<>(); + if (source == null) { + return copy; + } + for (Map.Entry> entry : source.entrySet()) { + if (entry.getKey() == null) { + continue; + } + Map group = + entry.getValue() == null ? new HashMap<>() : new HashMap<>(entry.getValue()); + copy.put(entry.getKey(), group); + } + return copy; + } + + @Override + public void setAttributesMap(Map> attributesMap) { + this.attributesMap = attributesMap != null ? attributesMap : new HashMap<>(); + } + + @Override + public Map> getAttributesMap() { + return attributesMap; + } + + @Override + public void setAttributes(String groupName, Map attributes) { + if (groupName == null) { + return; + } + attributesMap.put(groupName, attributes != null ? attributes : new HashMap<>()); + } + + @Override + public void setAttribute(String groupName, String key, String value) { + if (groupName == null || key == null) { + return; + } + Map attributes = attributesMap.get(groupName); + if (attributes == null) { + attributes = new HashMap<>(); + attributesMap.put(groupName, attributes); + } + attributes.put(key, value); + } + + @Override + public Map getAttributes(String groupName) { + return attributesMap.get(groupName); + } + + @Override + public String getAttribute(String groupName, String key) { + Map attributes = attributesMap.get(groupName); + if (attributes == null) { + return null; + } + return attributes.get(key); + } + + public List getConfigurationFiles() { + return configurationFiles; + } + + public void setConfigurationFiles(List configurationFiles) { + this.configurationFiles = + configurationFiles != null ? new ArrayList<>(configurationFiles) : new ArrayList<>(); + } +} diff --git a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java index c68f78f705b..c145a789a83 100644 --- a/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java +++ b/core/src/main/java/org/apache/hop/core/extension/HopExtensionPoint.java @@ -145,6 +145,23 @@ public enum HopExtensionPoint { HopGuiProjectAfterEnabled("Called after a project is enabled in Hop GUI"), + /** + * Called after a project (and optional lifecycle environment) is enabled. Payload is {@link + * org.apache.hop.core.AttributesContext}: identity fields plus namespaced {@link + * org.apache.hop.core.IAttributes} groups for optional plugins (marketplace, resource checks, …). + * Thrown {@link org.apache.hop.core.exception.HopException} aborts enablement. + */ + HopProjectEnvironmentAfterEnabled( + "Called after a project/lifecycle environment is enabled (AttributesContext)"), + + /** + * Called when the lifecycle environment dialog builds its tab folder so optional plugins can + * contribute tabs. Payload is a GUI extension object (see ui module) that carries a mutable + * {@link org.apache.hop.core.AttributesContext}. + */ + HopGuiLifecycleEnvironmentDialogTabs( + "Contribute tabs to the lifecycle environment dialog (AttributesContext)"), + HopGuiGetControlSpaceSortOrderPrefix( "Gets a prefix to steer the sort order of variables when using CTRL-SPACE. Defaults range from 900_ to 400_. Set prefixes in Map"), diff --git a/core/src/test/java/org/apache/hop/core/AttributesContextTest.java b/core/src/test/java/org/apache/hop/core/AttributesContextTest.java new file mode 100644 index 00000000000..1deaa1fea06 --- /dev/null +++ b/core/src/test/java/org/apache/hop/core/AttributesContextTest.java @@ -0,0 +1,76 @@ +/* + * 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.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class AttributesContextTest { + + @Test + void setAndGetAttribute() { + AttributesContext ctx = new AttributesContext(); + ctx.setAttribute("marketplace", "onEnable", "enforce"); + assertEquals("enforce", ctx.getAttribute("marketplace", "onEnable")); + assertNull(ctx.getAttribute("marketplace", "missing")); + assertNull(ctx.getAttribute("other", "onEnable")); + } + + @Test + void copyAttributesFromIsDeep() { + AttributesContext source = new AttributesContext(); + source.setAttribute("resources", "minDiskGb", "10"); + Map group = source.getAttributes("resources"); + assertNotNull(group); + + AttributesContext target = new AttributesContext(source); + assertEquals("10", target.getAttribute("resources", "minDiskGb")); + + // Mutating source group must not change the copy + group.put("minDiskGb", "99"); + assertEquals("10", target.getAttribute("resources", "minDiskGb")); + assertNotSame(source.getAttributesMap(), target.getAttributesMap()); + } + + @Test + void copyAttributesTo() { + AttributesContext source = new AttributesContext(); + source.setAttribute("marketplace", "envFile", "hop-env.yaml"); + + AttributesContext target = new AttributesContext(); + source.copyAttributesTo(target); + assertEquals("hop-env.yaml", target.getAttribute("marketplace", "envFile")); + } + + @Test + void setAttributesReplacesGroup() { + AttributesContext ctx = new AttributesContext(); + ctx.setAttribute("g", "a", "1"); + Map replacement = new HashMap<>(); + replacement.put("b", "2"); + ctx.setAttributes("g", replacement); + assertNull(ctx.getAttribute("g", "a")); + assertEquals("2", ctx.getAttribute("g", "b")); + } +} diff --git a/docker/integration-tests/unit-tests.Dockerfile b/docker/integration-tests/unit-tests.Dockerfile index c10cccd66b3..90e58676f12 100644 --- a/docker/integration-tests/unit-tests.Dockerfile +++ b/docker/integration-tests/unit-tests.Dockerfile @@ -74,6 +74,10 @@ RUN apt-get update \ COPY --chown=${JENKINS_USER}:${JENKINS_GROUP} ./assemblies/client/target/hop ${DEPLOYMENT_PATH}/hop +# Wave 1 optional plugins (marketplace) — expect host to have run tools/install-wave1-plugins.sh +# into assemblies/client/target/hop before this build (run-tests-docker.sh and Jenkins do this). +# If a plugin is still missing at runtime, corresponding ITs will fail clearly. + # Copy gcp key COPY --chown=${JENKINS_USER}:${JENKINS_GROUP} ${GCP_KEY_FILE} /tmp/google-key-apache-hop-it.json diff --git a/docker/marketplace-nexus/.gitignore b/docker/marketplace-nexus/.gitignore new file mode 100644 index 00000000000..4c49bd78f1d --- /dev/null +++ b/docker/marketplace-nexus/.gitignore @@ -0,0 +1 @@ +.env diff --git a/docker/marketplace-nexus/README.md b/docker/marketplace-nexus/README.md new file mode 100644 index 00000000000..95321f6d434 --- /dev/null +++ b/docker/marketplace-nexus/README.md @@ -0,0 +1,172 @@ + + +# Local Nexus for Hop Marketplace testing + +Sonatype **Nexus Repository OSS** is the supported local Maven repository for +developing and testing the Hop plugin marketplace. + +It is fully scriptable (no Pro-only APIs, no Postgres, no join keys). + +## Security model + +| Action | Who | +|--------|-----| +| `hop marketplace install` / GUI install | **Anonymous** read | +| `publish-marketplace-plugins.sh` | **admin** (or a deploy user) | + +Do not put admin credentials in day-to-day `hop-config.json`. + +## Start + +```bash +./docker/marketplace-nexus/start.sh + +# Wipe data and re-bootstrap: +./docker/marketplace-nexus/start.sh --reset + +# Custom admin password: +NEXUS_ADMIN_PASSWORD='secret' ./docker/marketplace-nexus/start.sh +``` + +Default after bootstrap: + +| | | +|--|--| +| UI | http://localhost:8081/ | +| Admin | `admin` / `hop-nexus-dev` (unless overridden) | +| Hosted repo | http://127.0.0.1:8081/repository/hop-plugins/ | + +Prefer **`127.0.0.1`** over `localhost` in Hop config: on some hosts `localhost` resolves +to IPv6 (`::1`) while Docker only publishes IPv4, which surfaces as +`java.net.ConnectException` from the marketplace client. + +First start can take **1–3 minutes** while Nexus initializes. + +## Publish marketplace plugin zips + +Plugin list and module paths come **only** from +`plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml`. +Add a plugin there once; this script packages/deploys it automatically. + +```bash +export NEXUS_PASSWORD=hop-nexus-dev # or your NEXUS_ADMIN_PASSWORD + +# Package every registry module, then deploy zips to local Nexus: +./docker/marketplace-nexus/publish-marketplace-plugins.sh --package + +# Or package yourself, then deploy only: +# ./mvnw -pl … -am package -DskipTests +./docker/marketplace-nexus/publish-marketplace-plugins.sh + +# Artifactory (or any Maven 2 repo): +# export ARTIFACTORY_URL='https://artifactory.example/artifactory/hop-plugins-local' +# export ARTIFACTORY_USER=deploy +# export ARTIFACTORY_PASSWORD=… +# export NEXUS_REPO_ID=artifactory +# ./docker/marketplace-nexus/publish-marketplace-plugins.sh --package +``` + +`publish-wave1-plugins.sh` remains as a deprecated alias of this script. + +## Hop marketplace (anonymous install) + +Config is always **`${HOP_CONFIG_FOLDER}/hop-config.json`**. Defaults are ASF +primary + Maven Central; add local Nexus and make it primary for dev: + +```bash +cd /path/to/hop # the unzipped hop-client directory +./hop marketplace repo add --id local-nexus \ + --url http://127.0.0.1:8081/repository/hop-plugins/ --primary +./hop marketplace repo list +./hop marketplace install hop-tech-parquet +# restore production defaults later: +./hop marketplace repo set-primary asf +# or: ./hop marketplace repo reset-defaults +``` + +Quick check: + +```bash +# Expect 200 or 404, not 401 +curl -sI "http://127.0.0.1:8081/repository/hop-plugins/" | head -1 +``` + +## Smoke test (CLI install / list / validate / apply / uninstall) + +```bash +# Nexus up + marketplace plugins published + hop client unzipped +./docker/marketplace-nexus/smoke-test.sh +``` + +Uses a temporary `HOP_CONFIG_FOLDER` (does not touch your real hop-config) and +installs two small plugins into `assemblies/client/target/hop` (override with +`HOP_DIR`). + +## Dummy staging (full registry verify + smoke) + +End-to-end dry-run of “stage plugin zips → confirm Maven layout → install”: + +```bash +./docker/marketplace-nexus/start.sh +export NEXUS_PASSWORD=hop-nexus-dev + +# Publish all optional-plugins.yaml zips, HTTP-verify each GAV, smoke install: +./docker/marketplace-nexus/dummy-staging.sh + +# Package modules first if zips are missing: +./docker/marketplace-nexus/dummy-staging.sh --package + +# Already published; only verify + smoke: +./docker/marketplace-nexus/dummy-staging.sh --skip-publish +``` + +After a real ASF `release:perform`, point the same checks at the staging repo: + +```bash +export REPO_URL='https://repository.apache.org/content/repositories/orgapachehop-XXXX/' +# or NEXUS_REPO_URL=… +./docker/marketplace-nexus/dummy-staging.sh --skip-publish +# then drop the staging repository if it was only a dry-run +``` + +## Scripts + +| Script | Role | +|--------|------| +| `start.sh` | Compose up + wait + configure | +| `configure-nexus.sh` | Anonymous + hosted repo (idempotent) | +| `publish-marketplace-plugins.sh` | `deploy-file` of all zips from `optional-plugins.yaml` | +| `publish-wave1-plugins.sh` | Deprecated alias of `publish-marketplace-plugins.sh` | +| `dummy-staging.sh` | Publish (optional) + verify all registry zips + install smoke | +| `smoke-test.sh` | Anonymous install / list / validate / apply / uninstall | + +## Stop + +```bash +docker compose -f docker/marketplace-nexus/docker-compose.yml down +# Wipe data: +docker compose -f docker/marketplace-nexus/docker-compose.yml down -v +``` + +## Production note + +Released Hop plugins still go through the **ASF release process** to +`repository.apache.org` → Maven Central. This Nexus stack only mimics Maven +layout for local development. diff --git a/docker/marketplace-nexus/configure-nexus.sh b/docker/marketplace-nexus/configure-nexus.sh new file mode 100755 index 00000000000..203bf250e67 --- /dev/null +++ b/docker/marketplace-nexus/configure-nexus.sh @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# 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 +# +# Idempotent Nexus OSS bootstrap for Hop marketplace: +# - change admin password from first-boot random password if needed +# - enable anonymous access +# - create hosted Maven repo hop-plugins (MIXED / ALLOW for SNAPSHOT zips) +# +# Env: +# NEXUS_URL default http://localhost:8081 +# NEXUS_ADMIN_USER default admin +# NEXUS_ADMIN_PASSWORD desired admin password (default hop-nexus-dev) +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_URL="${NEXUS_URL:-http://localhost:8081}" +BASE_URL="${BASE_URL%/}" +ADMIN_USER="${NEXUS_ADMIN_USER:-admin}" +ADMIN_PASSWORD="${NEXUS_ADMIN_PASSWORD:-hop-nexus-dev}" +REPO_NAME="${NEXUS_REPO_NAME:-hop-plugins}" +CONTAINER="${NEXUS_CONTAINER:-hop-marketplace-nexus}" + +api() { + # api METHOD PATH [curl args...] + local method="$1" + local path="$2" + shift 2 + curl -sS -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + -X "${method}" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + "${BASE_URL}${path}" \ + "$@" +} + +http_code() { + local method="$1" + local path="$2" + shift 2 + curl -sS -o /tmp/nexus-http.body -w '%{http_code}' \ + -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + -X "${method}" \ + -H 'Content-Type: application/json' \ + -H 'Accept: application/json' \ + "${BASE_URL}${path}" \ + "$@" +} + +echo "==> Resolving admin password" +CURRENT_PASSWORD="${ADMIN_PASSWORD}" + +# If desired password already works, skip change +code=$(curl -sS -o /dev/null -w '%{http_code}' -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + "${BASE_URL}/service/rest/v1/status" || echo 000) +if [[ "${code}" != "200" ]]; then + # First-boot password is in the data volume + if docker exec "${CONTAINER}" test -f /nexus-data/admin.password 2>/dev/null; then + BOOT_PASSWORD=$(docker exec "${CONTAINER}" cat /nexus-data/admin.password | tr -d '\r\n') + echo " Using first-boot password from container volume" + CURRENT_PASSWORD="${BOOT_PASSWORD}" + code=$(curl -sS -o /dev/null -w '%{http_code}' -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + "${BASE_URL}/service/rest/v1/status" || echo 000) + fi +fi + +if [[ "${code}" != "200" ]]; then + echo "ERROR: cannot authenticate as ${ADMIN_USER} (HTTP ${code})." >&2 + echo " If you set a custom password earlier, export NEXUS_ADMIN_PASSWORD=…" >&2 + echo " Or reset: ./start.sh --reset" >&2 + exit 1 +fi +echo " Authenticated as ${ADMIN_USER}" + +# Change password if still on first-boot password +if [[ "${CURRENT_PASSWORD}" != "${ADMIN_PASSWORD}" ]]; then + echo "==> Setting admin password" + chg=$(curl -sS -o /tmp/nexus-chg.body -w '%{http_code}' \ + -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + -X PUT \ + -H 'Content-Type: text/plain' \ + --data-binary "${ADMIN_PASSWORD}" \ + "${BASE_URL}/service/rest/v1/security/users/${ADMIN_USER}/change-password" || echo 000) + # Some versions expect JSON body: + if [[ "${chg}" != "204" && "${chg}" != "200" ]]; then + chg=$(curl -sS -o /tmp/nexus-chg.body -w '%{http_code}' \ + -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + -X PUT \ + -H 'Content-Type: application/json' \ + -d "{\"password\":\"${ADMIN_PASSWORD}\"}" \ + "${BASE_URL}/service/rest/v1/security/users/${ADMIN_USER}/change-password" || echo 000) + fi + if [[ "${chg}" == "204" || "${chg}" == "200" ]]; then + echo " Password updated" + CURRENT_PASSWORD="${ADMIN_PASSWORD}" + else + echo " WARNING: change-password returned HTTP ${chg} (body: $(cat /tmp/nexus-chg.body 2>/dev/null || true))" + echo " Continuing with current credentials" + fi +fi + +echo "==> Enabling anonymous access" +anon=$(http_code PUT /service/rest/v1/security/anonymous \ + -d '{"enabled":true,"userId":"anonymous","realmName":"NexusAuthorizingRealm"}') +if [[ "${anon}" == "200" || "${anon}" == "204" ]]; then + echo " Anonymous enabled (HTTP ${anon})" +else + # GET then PUT full object if needed + echo " WARNING: anonymous PUT HTTP ${anon}; check UI Security → Anonymous Access" + cat /tmp/nexus-http.body 2>/dev/null || true +fi + +echo "==> Ensuring hosted Maven repository '${REPO_NAME}'" +exists=$(curl -sS -o /dev/null -w '%{http_code}' -u "${ADMIN_USER}:${CURRENT_PASSWORD}" \ + "${BASE_URL}/service/rest/v1/repositories/${REPO_NAME}" || echo 000) +if [[ "${exists}" == "200" ]]; then + echo " Repository already exists" +else + create=$(http_code POST /service/rest/v1/repositories/maven/hosted -d "{ + \"name\": \"${REPO_NAME}\", + \"online\": true, + \"storage\": { + \"blobStoreName\": \"default\", + \"strictContentTypeValidation\": false, + \"writePolicy\": \"ALLOW\" + }, + \"cleanup\": { \"policyNames\": [] }, + \"component\": { \"proprietaryComponents\": false }, + \"maven\": { + \"versionPolicy\": \"MIXED\", + \"layoutPolicy\": \"PERMISSIVE\", + \"contentDisposition\": \"ATTACHMENT\" + } + }") + if [[ "${create}" == "201" || "${create}" == "200" ]]; then + echo " Created hosted Maven repo '${REPO_NAME}'" + else + echo "ERROR: create repository failed HTTP ${create}" >&2 + cat /tmp/nexus-http.body >&2 || true + exit 1 + fi +fi + +echo "==> Smoke test anonymous GET" +REPO_URL="${BASE_URL}/repository/${REPO_NAME}/" +anon_get=$(curl -sS -o /dev/null -w '%{http_code}' "${REPO_URL}" || echo 000) +case "${anon_get}" in + 200|404) + echo " OK — anonymous can reach ${REPO_URL} (HTTP ${anon_get})" + ;; + 401|403) + echo " WARNING: anonymous still denied (HTTP ${anon_get})." + echo " UI: Settings → Security → Anonymous Access → enable" + ;; + *) + echo " Unexpected HTTP ${anon_get} for ${REPO_URL}" + ;; +esac + +# Persist for publish scripts +ENV_FILE="${SCRIPT_DIR}/.env" +cat > "${ENV_FILE}" </dev/null || true +echo " Wrote ${ENV_FILE}" + +echo "Configure complete." diff --git a/docker/marketplace-nexus/docker-compose.yml b/docker/marketplace-nexus/docker-compose.yml new file mode 100644 index 00000000000..e8711a0d0fa --- /dev/null +++ b/docker/marketplace-nexus/docker-compose.yml @@ -0,0 +1,32 @@ +# 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 +# +# Local Sonatype Nexus Repository OSS for Hop marketplace testing. +# Usage: ./docker/marketplace-nexus/start.sh +# +services: + nexus: + image: sonatype/nexus3:3.76.1 + container_name: hop-marketplace-nexus + ports: + - "8081:8081" + volumes: + - hop-nexus-data:/nexus-data + # Nexus needs a few GB free under the volume; reduce heap for laptops if needed + environment: + INSTALL4J_ADD_VM_PARAMS: "-Xms512m -Xmx1536m -XX:MaxDirectMemorySize=1g -Djava.util.prefs.userRoot=/nexus-data/javaprefs" + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8081/service/rest/v1/status"] + interval: 15s + timeout: 10s + retries: 40 + start_period: 120s + +volumes: + hop-nexus-data: diff --git a/docker/marketplace-nexus/dummy-staging.sh b/docker/marketplace-nexus/dummy-staging.sh new file mode 100755 index 00000000000..11e51e317fb --- /dev/null +++ b/docker/marketplace-nexus/dummy-staging.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# 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 +# +# Dummy marketplace staging: publish optional-plugin zips (from the registry) to +# a Maven repo, verify HTTP layout, and smoke-test hop marketplace install. +# +# Default target is local Sonatype Nexus (same as publish-marketplace-plugins.sh). +# This is the dry-run analogue of an ASF Nexus staging repository check. +# +# Prerequisites: +# - ./docker/marketplace-nexus/start.sh (for default local Nexus) +# - Plugin zips built, or pass --package +# - Optional: hop client at assemblies/client/target/hop (or HOP_DIR) for install smoke +# +# Usage (repo root): +# ./docker/marketplace-nexus/dummy-staging.sh +# ./docker/marketplace-nexus/dummy-staging.sh --package +# ./docker/marketplace-nexus/dummy-staging.sh --skip-publish # only verify + smoke +# ./docker/marketplace-nexus/dummy-staging.sh --skip-smoke +# +# ASF staging (when you have apache.releases.https credentials): +# After release:perform, set: +# REPO_URL=https://repository.apache.org/content/repositories/orgapachehop-XXXX/ +# ./docker/marketplace-nexus/dummy-staging.sh --skip-publish +# Then drop the staging repo if it was only a dry-run. +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIST_SCRIPT="${ROOT}/tools/list-marketplace-plugins.sh" +PUBLISH_SCRIPT="${SCRIPT_DIR}/publish-marketplace-plugins.sh" + +if [[ -f "${SCRIPT_DIR}/.env" ]]; then + # shellcheck disable=SC1091 + set -a + source "${SCRIPT_DIR}/.env" + set +a +fi + +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" +GROUP_ID="${GROUP_ID:-org.apache.hop}" +# NEXUS_REPO_URL / ARTIFACTORY_URL / REPO_URL (ASF staging) all accepted +REPO_URL="${NEXUS_REPO_URL:-${ARTIFACTORY_URL:-${REPO_URL:-http://127.0.0.1:8081/repository/hop-plugins}}}" +REPO_URL="${REPO_URL%/}/" +HOP_DIR="${HOP_DIR:-${ROOT}/assemblies/client/target/hop}" + +PACKAGE=0 +SKIP_PUBLISH=0 +SKIP_SMOKE=0 +SMOKE_A="${SMOKE_PLUGIN_A:-hop-tech-dropbox}" +SMOKE_B="${SMOKE_PLUGIN_B:-hop-transform-fake}" + +for arg in "$@"; do + case "${arg}" in + --package) PACKAGE=1 ;; + --skip-publish) SKIP_PUBLISH=1 ;; + --skip-smoke) SKIP_SMOKE=1 ;; + -h | --help) + sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unknown argument: ${arg}" >&2 + exit 2 + ;; + esac +done + +echo "==> Dummy marketplace staging" +echo " version: ${VERSION}" +echo " repo: ${REPO_URL}" +echo " hop dir: ${HOP_DIR}" +echo + +if [[ ! -x "${LIST_SCRIPT}" ]]; then + chmod +x "${LIST_SCRIPT}" 2>/dev/null || true +fi + +mapfile -t ENTRIES < <(HOP_VERSION="${VERSION}" "${LIST_SCRIPT}" --zips) +if [[ ${#ENTRIES[@]} -eq 0 ]]; then + echo "No plugins in optional-plugins.yaml registry" >&2 + exit 1 +fi +echo " registry: ${#ENTRIES[@]} plugin(s)" + +# --- Publish (local Nexus / Artifactory) --- +if [[ "${SKIP_PUBLISH}" -eq 0 ]]; then + echo + echo "==> Publish marketplace plugin zips" + if [[ ! -f "${PUBLISH_SCRIPT}" ]]; then + echo "Missing ${PUBLISH_SCRIPT}" >&2 + exit 1 + fi + publish_args=() + if [[ "${PACKAGE}" -eq 1 ]]; then + publish_args+=(--package) + fi + # Prefer 127.0.0.1 for deploy URL consistency with Hop client + export NEXUS_REPO_URL="${REPO_URL%/}" + export HOP_VERSION="${VERSION}" + "${PUBLISH_SCRIPT}" "${publish_args[@]+"${publish_args[@]}"}" +else + echo + echo "==> Skip publish (--skip-publish)" +fi + +# --- Verify HTTP Maven layout for every registry plugin --- +echo +echo "==> Verify zip URLs (Maven layout)" +ok=0 +fail=0 +declare -a FAILED=() +for entry in "${ENTRIES[@]}"; do + artifactId="${entry%%|*}" + # SNAPSHOT path uses version folder; unique timestamp filename resolved via maven-metadata later. + # For releases: {base}{g}/{a}/{v}/{a}-{v}.zip + # For SNAPSHOT: try literal SNAPSHOT zip first, then accept 200 on metadata + any zip listing. + gpath="${GROUP_ID//.//}" + base="${REPO_URL}${gpath}/${artifactId}/${VERSION}/" + literal_zip="${base}${artifactId}-${VERSION}.zip" + code=$(curl -sS -o /dev/null -w '%{http_code}' -L "${literal_zip}" || true) + if [[ "${code}" == "200" ]]; then + ok=$((ok + 1)) + echo " OK ${artifactId} (HTTP ${code})" + continue + fi + # SNAPSHOT unique versions: maven-metadata.xml lists zip + meta="${base}maven-metadata.xml" + mcode=$(curl -sS -o /tmp/hop-staging-meta.xml -w '%{http_code}' -L "${meta}" || true) + if [[ "${mcode}" == "200" ]]; then + # Prefer value that appears with extension zip (snapshotVersion block) + value=$( + tr '\n' ' ' /\n/g' \ + | grep -F 'zip' \ + | sed -n 's/.*\([^<]*\)<\/value>.*/\1/p' \ + | head -1 || true + ) + if [[ -z "${value}" ]]; then + value=$(sed -n 's/.*\([^<]*\)<\/value>.*/\1/p' /tmp/hop-staging-meta.xml | head -1 || true) + fi + if [[ -n "${value}" ]]; then + snap_zip="${base}${artifactId}-${value}.zip" + scode=$(curl -sS -o /dev/null -w '%{http_code}' -L "${snap_zip}" || true) + if [[ "${scode}" == "200" ]]; then + ok=$((ok + 1)) + echo " OK ${artifactId} (unique SNAPSHOT ${value}.zip)" + continue + fi + FAILED+=("${artifactId} snap_zip=${scode} value=${value}") + echo " FAIL ${artifactId} (SNAPSHOT zip HTTP ${scode} for ${value})" + fail=$((fail + 1)) + continue + fi + fi + fail=$((fail + 1)) + FAILED+=("${artifactId} literal=${code} meta=${mcode:-n/a}") + echo " FAIL ${artifactId} (zip HTTP ${code}, meta HTTP ${mcode:-n/a})" +done +rm -f /tmp/hop-staging-meta.xml + +echo +echo " verified OK=${ok} FAIL=${fail} (of ${#ENTRIES[@]})" +if [[ "${fail}" -gt 0 ]]; then + echo "Missing or unreachable zips:" >&2 + printf ' - %s\n' "${FAILED[@]}" >&2 + exit 1 +fi + +# --- Hop marketplace smoke install --- +if [[ "${SKIP_SMOKE}" -eq 1 ]]; then + echo + echo "==> Skip install smoke (--skip-smoke)" + echo "OK — dummy staging verify passed against ${REPO_URL}" + exit 0 +fi + +if [[ ! -x "${HOP_DIR}/hop" ]]; then + echo + echo "WARN: Hop launcher not found at ${HOP_DIR}/hop — skip install smoke." + echo " Build/unzip hop-client or set HOP_DIR." + echo "OK — dummy staging publish+verify passed against ${REPO_URL}" + exit 0 +fi + +# Ensure marketplace CLI plugin is present in the hop install (bundled plugin). +MKT_ZIP="${ROOT}/plugins/misc/marketplace/target/hop-misc-marketplace-${VERSION}.zip" +if [[ ! -f "${HOP_DIR}/plugins/misc/marketplace/version.xml" ]]; then + if [[ -f "${MKT_ZIP}" ]]; then + echo "==> Installing marketplace plugin into ${HOP_DIR}" + unzip -o -q "${MKT_ZIP}" -d "${HOP_DIR}" + else + echo "WARN: marketplace plugin not in hop install and zip missing: ${MKT_ZIP}" >&2 + echo " Smoke may fail with 'Unmatched arguments: marketplace'." >&2 + fi +fi + +echo +echo "==> Hop marketplace install smoke (${SMOKE_A}, ${SMOKE_B})" +export NEXUS_REPO_URL="${REPO_URL}" +export HOP_DIR +export HOP_VERSION="${VERSION}" +export SMOKE_PLUGIN_A="${SMOKE_A}" +export SMOKE_PLUGIN_B="${SMOKE_B}" +# Prefer 127.0.0.1 for Hop client (IPv6 localhost issues with Docker) +export NEXUS_REPO_URL="${REPO_URL/localhost/127.0.0.1}" +"${SCRIPT_DIR}/smoke-test.sh" + +echo +echo "================================================================" +echo "Dummy staging complete." +echo " Repository: ${REPO_URL}" +echo " Registry plugins verified: ${ok}" +echo +echo "ASF release staging (RM) — after release:perform:" +echo " 1. Open https://repository.apache.org/#stagingRepositories" +echo " 2. Open the orgapachehop-* staging repo content" +echo " 3. Confirm e.g. org/apache/hop/hop-tech-parquet//…zip" +echo " 4. REPO_URL=/ ./docker/marketplace-nexus/dummy-staging.sh --skip-publish" +echo " 5. Drop the staging repository if this was only a dry-run" +echo "================================================================" diff --git a/docker/marketplace-nexus/publish-marketplace-plugins.sh b/docker/marketplace-nexus/publish-marketplace-plugins.sh new file mode 100755 index 00000000000..bb1e461dd64 --- /dev/null +++ b/docker/marketplace-nexus/publish-marketplace-plugins.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# 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 +# +# Deploy marketplace-optional plugin zips to local Nexus, Artifactory, or any +# Maven 2 repository. Plugin list and module paths come only from: +# +# plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml +# +# Env: +# NEXUS_REPO_URL or ARTIFACTORY_URL e.g. http://127.0.0.1:8081/repository/hop-plugins +# NEXUS_USER / NEXUS_PASSWORD (or ARTIFACTORY_USER / ARTIFACTORY_PASSWORD) +# NEXUS_REPO_ID Maven server id in settings (default hop-plugins) +# HOP_VERSION default 2.19.0-SNAPSHOT +# GROUP_ID default org.apache.hop (or groupId: in registry) +# +# Flags: +# --package run mvn package for all registry modules before deploy +# --dry-run print what would be deployed without calling Maven +# +# Usage (from repo root): +# export NEXUS_PASSWORD=hop-nexus-dev +# ./docker/marketplace-nexus/publish-marketplace-plugins.sh +# ./docker/marketplace-nexus/publish-marketplace-plugins.sh --package +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LIST_SCRIPT="${ROOT}/tools/list-marketplace-plugins.sh" +REGISTRY="${ROOT}/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml" + +PACKAGE=0 +DRY_RUN=0 +for arg in "$@"; do + case "${arg}" in + --package) PACKAGE=1 ;; + --dry-run) DRY_RUN=1 ;; + -h | --help) + sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) + echo "Unknown argument: ${arg}" >&2 + echo "Usage: $0 [--package] [--dry-run]" >&2 + exit 2 + ;; + esac +done + +# Capture caller overrides before optional .env (local docker defaults must not win). +_OV_REPO_URL="${NEXUS_REPO_URL:-${ARTIFACTORY_URL:-}}" +_OV_REPO_ID="${NEXUS_REPO_ID:-}" +_OV_USER="${NEXUS_USER:-${ARTIFACTORY_USER:-}}" +_OV_PASS="${NEXUS_PASSWORD:-${ARTIFACTORY_PASSWORD:-}}" +_OV_VERSION="${HOP_VERSION:-}" + +# Load .env from start/configure if present (defaults for local Nexus only) +if [[ -f "${SCRIPT_DIR}/.env" ]]; then + # shellcheck disable=SC1091 + set -a + # shellcheck source=/dev/null + source "${SCRIPT_DIR}/.env" + set +a +fi + +# Restore explicit environment / caller exports +[[ -n "${_OV_REPO_URL}" ]] && NEXUS_REPO_URL="${_OV_REPO_URL}" && ARTIFACTORY_URL="${_OV_REPO_URL}" +[[ -n "${_OV_REPO_ID}" ]] && NEXUS_REPO_ID="${_OV_REPO_ID}" +[[ -n "${_OV_USER}" ]] && NEXUS_USER="${_OV_USER}" && ARTIFACTORY_USER="${_OV_USER}" +[[ -n "${_OV_PASS}" ]] && NEXUS_PASSWORD="${_OV_PASS}" && ARTIFACTORY_PASSWORD="${_OV_PASS}" +[[ -n "${_OV_VERSION}" ]] && HOP_VERSION="${_OV_VERSION}" + +REPO_URL="${NEXUS_REPO_URL:-${ARTIFACTORY_URL:-http://127.0.0.1:8081/repository/hop-plugins}}" +REPO_URL="${REPO_URL%/}" +REPO_ID="${NEXUS_REPO_ID:-hop-plugins}" +USER="${NEXUS_USER:-${NEXUS_ADMIN_USER:-${ARTIFACTORY_USER:-admin}}}" +PASS="${NEXUS_PASSWORD:-${NEXUS_ADMIN_PASSWORD:-${ARTIFACTORY_PASSWORD:-}}}" +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" + +echo "Deploy target: ${REPO_URL} (server id=${REPO_ID}, user=${USER})" + +# Prefer groupId from registry when set +GROUP_ID="${GROUP_ID:-}" +if [[ -z "${GROUP_ID}" && -f "${REGISTRY}" ]]; then + GROUP_ID="$(sed -n 's/^groupId:[[:space:]]*//p' "${REGISTRY}" | head -1 | tr -d '[:space:]"'"'"'')" +fi +GROUP_ID="${GROUP_ID:-org.apache.hop}" + +if [[ ! -x "${LIST_SCRIPT}" ]]; then + chmod +x "${LIST_SCRIPT}" 2>/dev/null || true +fi +if [[ ! -f "${LIST_SCRIPT}" ]]; then + echo "Missing ${LIST_SCRIPT}" >&2 + exit 1 +fi + +mapfile -t ENTRIES < <(HOP_VERSION="${VERSION}" "${LIST_SCRIPT}" --zips) +if [[ ${#ENTRIES[@]} -eq 0 ]]; then + echo "No plugins found in ${REGISTRY}" >&2 + exit 1 +fi + +MVN="${ROOT}/mvnw" +if [[ ! -x "${MVN}" ]]; then + MVN=mvn +fi + +if [[ "${PACKAGE}" -eq 1 ]]; then + mapfile -t MODULES < <("${LIST_SCRIPT}" --modules | awk 'NF && !seen[$0]++') + if [[ ${#MODULES[@]} -eq 0 ]]; then + echo "No modulePath entries to package" >&2 + exit 1 + fi + PL_LIST="$(IFS=,; echo "${MODULES[*]}")" + echo "Packaging ${#MODULES[@]} module(s) (HOP_VERSION=${VERSION})…" + if [[ "${DRY_RUN}" -eq 1 ]]; then + echo "DRY-RUN: ${MVN} -pl ${PL_LIST} -am package -DskipTests" + else + "${MVN}" -pl "${PL_LIST}" -am package -DskipTests + fi +fi + +if [[ "${DRY_RUN}" -eq 0 && -z "${PASS}" ]]; then + echo "Set NEXUS_PASSWORD (or NEXUS_ADMIN_PASSWORD / ARTIFACTORY_PASSWORD)." >&2 + echo "After ./docker/marketplace-nexus/start.sh the default is hop-nexus-dev." >&2 + exit 1 +fi + +SETTINGS="" +if [[ "${DRY_RUN}" -eq 0 ]]; then + SETTINGS="$(mktemp)" + trap 'rm -f "${SETTINGS}"' EXIT + cat >"${SETTINGS}" < + + + ${REPO_ID} + ${USER} + ${PASS} + + + +EOF +fi + +deploy_one() { + local artifactId="$1" + local rel="$2" + local file="${ROOT}/${rel}" + if [[ ! -f "${file}" ]]; then + echo "SKIP (not built): ${artifactId} — missing ${rel}" + return 0 + fi + echo "Deploying ${GROUP_ID}:${artifactId}:${VERSION} → ${REPO_URL}" + if [[ "${DRY_RUN}" -eq 1 ]]; then + echo "DRY-RUN: deploy-file ${file}" + return 0 + fi + "${MVN}" -q -s "${SETTINGS}" \ + org.apache.maven.plugins:maven-deploy-plugin:3.1.3:deploy-file \ + -DgroupId="${GROUP_ID}" \ + -DartifactId="${artifactId}" \ + -Dversion="${VERSION}" \ + -Dpackaging=zip \ + -Dfile="${file}" \ + -DrepositoryId="${REPO_ID}" \ + -Durl="${REPO_URL}" \ + -DgeneratePom=true \ + -DretryFailedDeploymentCount=2 +} + +deployed=0 +skipped=0 +for entry in "${ENTRIES[@]}"; do + artifactId="${entry%%|*}" + rel="${entry#*|}" + if [[ ! -f "${ROOT}/${rel}" ]]; then + echo "SKIP (not built): ${artifactId} — missing ${rel}" + skipped=$((skipped + 1)) + continue + fi + deploy_one "${artifactId}" "${rel}" + deployed=$((deployed + 1)) +done + +echo "Done. deployed=${deployed} skipped=${skipped} base URL: ${REPO_URL}/" +echo "Example: hop marketplace install hop-tech-parquet" +echo "Registry: ${REGISTRY}" diff --git a/docker/marketplace-nexus/publish-wave1-plugins.sh b/docker/marketplace-nexus/publish-wave1-plugins.sh new file mode 100755 index 00000000000..6e7ccd3bb7a --- /dev/null +++ b/docker/marketplace-nexus/publish-wave1-plugins.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# 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 +# +# Deprecated name: use publish-marketplace-plugins.sh +# Forwards all arguments and environment to the registry-driven publisher. +# +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +echo "NOTE: publish-wave1-plugins.sh is deprecated; use publish-marketplace-plugins.sh" >&2 +exec "${SCRIPT_DIR}/publish-marketplace-plugins.sh" "$@" diff --git a/docker/marketplace-nexus/smoke-test.sh b/docker/marketplace-nexus/smoke-test.sh new file mode 100755 index 00000000000..6b3eccf8d23 --- /dev/null +++ b/docker/marketplace-nexus/smoke-test.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# 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 +# +# End-to-end smoke test: local Nexus + hop marketplace CLI (anonymous). +# +# Prerequisites: +# - ./docker/marketplace-nexus/start.sh +# - ./docker/marketplace-nexus/publish-marketplace-plugins.sh +# - Unzipped hop client at assemblies/client/target/hop (or HOP_DIR) +# +# Usage (from repo root): +# ./docker/marketplace-nexus/smoke-test.sh +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +HOP_DIR="${HOP_DIR:-${ROOT}/assemblies/client/target/hop}" +REPO_URL="${NEXUS_REPO_URL:-http://127.0.0.1:8081/repository/hop-plugins/}" +REPO_URL="${REPO_URL%/}/" +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" + +# Small plugins for a fast round-trip (publish-marketplace-plugins must have deployed them) +INSTALL_A="${SMOKE_PLUGIN_A:-hop-tech-dropbox}" +INSTALL_B="${SMOKE_PLUGIN_B:-hop-transform-edi2xml}" + +if [[ ! -x "${HOP_DIR}/hop" ]]; then + echo "Hop launcher not found: ${HOP_DIR}/hop" >&2 + echo "Build/unzip hop-client first (assemblies/client/target/hop)." >&2 + exit 1 +fi + +echo "==> Checking Nexus anonymous read: ${REPO_URL}" +code=$(curl -sS -o /dev/null -w '%{http_code}' "${REPO_URL}" || true) +if [[ "${code}" != "200" && "${code}" != "404" ]]; then + echo "Nexus not reachable or not anonymous (HTTP ${code}). Run start.sh first." >&2 + exit 1 +fi + +# Isolated config so we do not touch the developer's HOP_CONFIG_FOLDER +CFG_DIR="$(mktemp -d "${TMPDIR:-/tmp}/hop-marketplace-smoke.XXXXXX")" +AUDIT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/hop-marketplace-audit.XXXXXX")" +trap 'rm -rf "${CFG_DIR}" "${AUDIT_DIR}"' EXIT + +cat > "${CFG_DIR}/hop-config.json" < "${ENV_FILE}" < marketplace list (baseline)" +run_hop marketplace list || true + +echo "==> marketplace install ${INSTALL_A}" +run_hop marketplace install "${INSTALL_A}" + +echo "==> marketplace install ${INSTALL_B}" +run_hop marketplace install "${INSTALL_B}" + +echo "==> marketplace list (expect both)" +list_out=$(run_hop marketplace list) +echo "${list_out}" +echo "${list_out}" | grep -q "${INSTALL_A}" +echo "${list_out}" | grep -q "${INSTALL_B}" + +echo "==> marketplace validate -f hop-env-smoke.yaml" +run_hop marketplace validate -f "${ENV_FILE}" + +echo "==> marketplace uninstall ${INSTALL_B}" +run_hop marketplace uninstall "${INSTALL_B}" + +echo "==> marketplace apply -f hop-env-smoke.yaml (idempotent ${INSTALL_A})" +run_hop marketplace apply -f "${ENV_FILE}" + +echo "==> marketplace list (expect ${INSTALL_A} only from smoke installs; others may remain)" +run_hop marketplace list | grep -q "${INSTALL_A}" + +if [[ ! -d "${HOP_DIR}/plugins" ]]; then + echo "FAIL: plugins/ missing under ${HOP_DIR}" >&2 + exit 1 +fi + +echo +echo "OK — marketplace smoke test passed against ${REPO_URL}" +echo " hop install: ${HOP_DIR}" +echo " config: ${HOP_CONFIG_FOLDER}/hop-config.json" diff --git a/docker/marketplace-nexus/start.sh b/docker/marketplace-nexus/start.sh new file mode 100755 index 00000000000..bd6ad60cef2 --- /dev/null +++ b/docker/marketplace-nexus/start.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# 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 +# +# Start Nexus, set admin password, create hop-plugins repo, enable anonymous read. +# +# Usage: +# ./docker/marketplace-nexus/start.sh +# ./docker/marketplace-nexus/start.sh --reset # wipe volumes +# NEXUS_ADMIN_PASSWORD=secret ./docker/marketplace-nexus/start.sh +# +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +BASE_URL="${NEXUS_URL:-http://localhost:8081}" +ADMIN_USER="${NEXUS_ADMIN_USER:-admin}" +# Password we want after bootstrap (override with env) +ADMIN_PASSWORD="${NEXUS_ADMIN_PASSWORD:-hop-nexus-dev}" +RESET=false + +for arg in "$@"; do + case "${arg}" in + --reset|-r) RESET=true ;; + -h|--help) + echo "Usage: $0 [--reset]" + echo " NEXUS_ADMIN_PASSWORD admin password to set (default: hop-nexus-dev)" + exit 0 + ;; + esac +done + +if [[ "${RESET}" == "true" ]]; then + echo "Resetting Nexus volumes..." + docker compose down -v +fi + +echo "Starting Nexus Repository OSS..." +docker compose up -d + +echo "Waiting for Nexus status API (first boot can take 1–3 minutes)..." +ready=false +for i in $(seq 1 80); do + if curl -sf "${BASE_URL}/service/rest/v1/status" >/dev/null 2>&1; then + ready=true + break + fi + # Also accept 200/503 from status/writable during warmup + code=$(curl -s -o /dev/null -w '%{http_code}' "${BASE_URL}/service/rest/v1/status" 2>/dev/null || echo 000) + if [[ "${code}" == "200" ]]; then + ready=true + break + fi + printf '.' + sleep 3 +done +echo +if [[ "${ready}" != "true" ]]; then + echo "ERROR: Nexus did not become ready. Logs:" >&2 + docker logs hop-marketplace-nexus 2>&1 | tail -40 >&2 + exit 1 +fi +echo "Nexus is up: ${BASE_URL}" + +export NEXUS_URL="${BASE_URL}" +export NEXUS_ADMIN_USER="${ADMIN_USER}" +export NEXUS_ADMIN_PASSWORD="${ADMIN_PASSWORD}" +./configure-nexus.sh + +echo +echo "Done." +echo " UI: ${BASE_URL}/" +echo " Admin user: ${ADMIN_USER}" +echo " Admin pass: ${ADMIN_PASSWORD}" +echo " Hosted repo: ${BASE_URL}/repository/hop-plugins/" +echo +echo "Publish (authenticated):" +echo " export NEXUS_URL=${BASE_URL}/repository/hop-plugins" +echo " export NEXUS_USER=${ADMIN_USER}" +echo " export NEXUS_PASSWORD=${ADMIN_PASSWORD}" +echo " ./publish-marketplace-plugins.sh [--package]" +echo +echo "Hop marketplace (anonymous read):" +echo " \"url\": \"${BASE_URL}/repository/hop-plugins/\"" diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile index 4fdc6d5d018..bb63c4a4107 100644 --- a/docker/unified.Dockerfile +++ b/docker/unified.Dockerfile @@ -193,7 +193,7 @@ RUN mkdir -p /build/hop-web-prepared/webapps/ROOT && \ cp -r /build/assemblies/client/target/hop/config /build/hop-web-prepared/webapps/ROOT/ && \ cp -r /build/assemblies/client/target/hop/plugins /build/hop-web-prepared/ && \ cp -r /build/assemblies/client/target/hop/lib/jdbc/ /build/hop-web-prepared/jdbc-drivers && \ - cp -r /build/assemblies/client/target/hop/lib/beam/* /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \ + if [ -d /build/assemblies/client/target/hop/plugins/engines/beam/lib-beam ]; then cp -r /build/assemblies/client/target/hop/plugins/engines/beam/lib-beam/* /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/; fi && \ cp -r /build/assemblies/client/target/hop/lib/core/* /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/ && \ rm /build/hop-web-prepared/webapps/ROOT/WEB-INF/lib/hop-ui-rcp* && \ cp /build/docker/resources/run-web.sh /build/hop-web-prepared/run-web.sh && \ diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile index 5b768204a90..abe4a9ae07c 100644 --- a/docker/web.Dockerfile +++ b/docker/web.Dockerfile @@ -77,7 +77,7 @@ RUN groupadd -r hop -g ${HOP_GID} \ COPY ./assemblies/web/target/webapp/ "${CATALINA_HOME}"/webapps/ROOT/ COPY ./assemblies/client/target/hop/config "${CATALINA_HOME}"/webapps/ROOT/config COPY ./assemblies/client/target/hop/lib/core "${CATALINA_HOME}"/webapps/ROOT/WEB-INF/lib -COPY ./assemblies/client/target/hop/lib/beam "${CATALINA_HOME}"/webapps/ROOT/WEB-INF/lib +COPY ./assemblies/client/target/hop/plugins/engines/beam/lib-beam "${CATALINA_HOME}"/webapps/ROOT/WEB-INF/lib COPY ./assemblies/client/target/hop/plugins "${CATALINA_HOME}"/plugins COPY ./assemblies/client/target/hop/lib/jdbc/ "${CATALINA_HOME}"/jdbc-drivers COPY --chown=hop ./docker/resources/run-web.sh /tmp/ diff --git a/docs/hop-dev-manual/modules/ROOT/examples/marketplace/plugin-env.example.yaml b/docs/hop-dev-manual/modules/ROOT/examples/marketplace/plugin-env.example.yaml new file mode 100644 index 00000000000..58b59c2e520 --- /dev/null +++ b/docs/hop-dev-manual/modules/ROOT/examples/marketplace/plugin-env.example.yaml @@ -0,0 +1,45 @@ +# 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. +# +# Source of truth for marketplace-optional plugins (not bundled in hop-client). +# full-client-env.yaml is generated from this list; GUI catalog loads it at runtime. +# When moving a plugin out of the fat assembly, add an entry here only. + +# Example hop-env for a third-party plugin published to a private Nexus. +# Adjust groupId, versions, and repository URL for your environment. +# +# ./hop marketplace apply -f plugin-env.example.yaml +# # restart Hop +# +version: "1.0" +# Hop client line users should run (optional Apache plugins use this as default version) +hopVersion: "2.19.0" +enforceOnRun: false + +repositories: + - id: sandbox + url: "https://nexus.sandbox.example/repository/hop-plugins/" + - id: asf + url: "https://repository.apache.org/content/groups/public/" + +plugins: + # Other plugins this product needs (install first) + - groupId: org.apache.hop + artifactId: hop-tech-parquet + version: "2.19.0" + # Your plugin (groupId:artifactId:version) + - groupId: com.example + artifactId: hop-data-vault + version: "1.2.0" diff --git a/docs/hop-dev-manual/modules/ROOT/examples/marketplace/release-plugin-to-nexus.sh b/docs/hop-dev-manual/modules/ROOT/examples/marketplace/release-plugin-to-nexus.sh new file mode 100755 index 00000000000..90e355535da --- /dev/null +++ b/docs/hop-dev-manual/modules/ROOT/examples/marketplace/release-plugin-to-nexus.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +# 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 +# +# Template: release a third-party Hop plugin zip to a Maven repository (Nexus). +# Copy into your plugin repo as scripts/release-to-nexus.sh and adjust defaults. +# +# Usage: +# export NEXUS_USER=ci NEXUS_PASSWORD=secret +# export NEXUS_URL=https://nexus.example/repository/hop-plugins/ +# ./release-plugin-to-nexus.sh 1.2.0 --hop-version 2.19.0 +# ./release-plugin-to-nexus.sh 1.2.0 --dry-run +# +set -euo pipefail + +GROUP_ID="${GROUP_ID:-com.example}" +ARTIFACT_ID="${ARTIFACT_ID:-hop-data-vault}" +# Path to the packaged zip after mvn package (override if your layout differs) +ZIP_GLOB="${ZIP_GLOB:-target/${ARTIFACT_ID}-*.zip}" + +NEXUS_URL="${NEXUS_URL:-http://127.0.0.1:8081/repository/hop-plugins/}" +NEXUS_REPO_ID="${NEXUS_REPO_ID:-sandbox-hop-plugins}" +HOP_VERSION="${HOP_VERSION:-2.19.0}" +DRY_RUN=false +DO_TAG=false +PLUGIN_VERSION="" + +usage() { + cat < [options] + + Plugin release version (e.g. 1.2.0 or 1.2.0-SNAPSHOT) + +Options: + --hop-version VER Hop APIs to compile/test against (default: ${HOP_VERSION}) + --nexus-url URL Maven repo URL (default: ${NEXUS_URL}) + --repo-id ID settings.xml server id (default: ${NEXUS_REPO_ID}) + --group-id G Maven groupId (default: ${GROUP_ID}) + --artifact-id A Maven artifactId (default: ${ARTIFACT_ID}) + --tag Create git tag plugin- after successful deploy + --dry-run Print actions only + -h, --help Show this help + +Environment: + NEXUS_USER / NEXUS_PASSWORD Deploy credentials (or configure settings.xml) + MVN Maven command (default: mvn) +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --hop-version) HOP_VERSION="$2"; shift 2 ;; + --nexus-url) NEXUS_URL="$2"; shift 2 ;; + --repo-id) NEXUS_REPO_ID="$2"; shift 2 ;; + --group-id) GROUP_ID="$2"; shift 2 ;; + --artifact-id) ARTIFACT_ID="$2"; shift 2 ;; + --tag) DO_TAG=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + -h|--help) usage; exit 0 ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + if [[ -z "${PLUGIN_VERSION}" ]]; then + PLUGIN_VERSION="$1" + shift + else + echo "Unexpected argument: $1" >&2 + exit 1 + fi + ;; + esac +done + +if [[ -z "${PLUGIN_VERSION}" ]]; then + echo "ERROR: plugin version is required" >&2 + usage >&2 + exit 1 +fi + +MVN="${MVN:-mvn}" +NEXUS_URL="${NEXUS_URL%/}/" + +run() { + if [[ "${DRY_RUN}" == "true" ]]; then + echo "DRY-RUN: $*" + else + "$@" + fi +} + +echo "==> Plugin ${GROUP_ID}:${ARTIFACT_ID}:${PLUGIN_VERSION}" +echo " Hop compile version: ${HOP_VERSION}" +echo " Nexus: ${NEXUS_URL} (repo id ${NEXUS_REPO_ID})" + +if [[ "${DRY_RUN}" != "true" ]]; then + if [[ -z "${NEXUS_USER:-}" || -z "${NEXUS_PASSWORD:-}" ]]; then + echo "NOTE: NEXUS_USER/NEXUS_PASSWORD not set; relying on Maven settings.xml server '${NEXUS_REPO_ID}'" + fi +fi + +# Optional: align Maven project version (requires versions-maven-plugin or CI-friendly versions) +# Uncomment if your pom version should match the release argument: +# run ${MVN} -B versions:set -DnewVersion="${PLUGIN_VERSION}" -DgenerateBackupPoms=false + +echo "==> verify (hop.version=${HOP_VERSION})" +run ${MVN} -B clean verify -Dhop.version="${HOP_VERSION}" + +echo "==> package zip" +run ${MVN} -B package -DskipTests -Dhop.version="${HOP_VERSION}" + +ZIP_FILE="" +# shellcheck disable=SC2086 +for f in ${ZIP_GLOB}; do + if [[ -f "$f" && "$f" == *"${PLUGIN_VERSION}.zip" ]]; then + ZIP_FILE="$f" + break + fi +done +if [[ -z "${ZIP_FILE}" ]]; then + # fallback: first matching zip + # shellcheck disable=SC2086 + for f in ${ZIP_GLOB}; do + if [[ -f "$f" ]]; then + ZIP_FILE="$f" + break + fi + done +fi + +if [[ -z "${ZIP_FILE}" || ! -f "${ZIP_FILE}" ]]; then + echo "ERROR: zip not found (looked for ${ZIP_GLOB}). Check assembly appendAssemblyId=false." >&2 + exit 1 +fi + +echo "==> deploy ${ZIP_FILE}" +DEPLOY=( + ${MVN} -B org.apache.maven.plugins:maven-deploy-plugin:3.1.3:deploy-file + -DgroupId="${GROUP_ID}" + -DartifactId="${ARTIFACT_ID}" + -Dversion="${PLUGIN_VERSION}" + -Dpackaging=zip + -Dfile="${ZIP_FILE}" + -DrepositoryId="${NEXUS_REPO_ID}" + -Durl="${NEXUS_URL}" + -DgeneratePom=true +) +if [[ -n "${NEXUS_USER:-}" && -n "${NEXUS_PASSWORD:-}" ]]; then + # Temporary settings so CI need not write ~/.m2/settings.xml + SETTINGS="$(mktemp)" + trap 'rm -f "${SETTINGS}"' EXIT + cat >"${SETTINGS}" < + + + ${NEXUS_REPO_ID} + ${NEXUS_USER} + ${NEXUS_PASSWORD} + + + +EOF + DEPLOY+=(-s "${SETTINGS}") +fi +run "${DEPLOY[@]}" + +if [[ "${DO_TAG}" == "true" ]]; then + TAG="plugin-${PLUGIN_VERSION}" + echo "==> git tag ${TAG}" + run git tag -a "${TAG}" -m "Release ${GROUP_ID}:${ARTIFACT_ID}:${PLUGIN_VERSION} (Hop ${HOP_VERSION})" +fi + +cat </hop-tech-parquet-.zip +org/apache/hop/hop-engines-spark//hop-engines-spark-.zip +---- + +The full optional list is in `plugins/misc/marketplace/.../optional-plugins.yaml`. + +Quick smoke (point marketplace at the **staging** repository URL as primary, then): + +[source,bash] +---- +./hop marketplace install hop-tech-parquet: +# or apply a small env file / full-client-env.yaml against staging +---- + +From a Hop **source** checkout with a built hop-client, you can automate the same +checks against local Nexus or an ASF staging URL: + +[source,bash] +---- +# Local dummy staging (publish + verify all optional-plugins.yaml zips + install smoke) +./docker/marketplace-nexus/start.sh +export NEXUS_PASSWORD=hop-nexus-dev +./docker/marketplace-nexus/dummy-staging.sh + +# Against an ASF staging repository (after release:perform): +export NEXUS_REPO_URL='https://repository.apache.org/content/repositories/orgapachehop-XXXX/' +./docker/marketplace-nexus/dummy-staging.sh --skip-publish +---- + +Do not use prune on a partial install. Restart Hop after install. Drop the staging +repository if this was only a dry-run validation. + == Extra checks You can also check for unwanted binary files on the checked out tag (note: this requires the .git folder to still be present) diff --git a/docs/hop-dev-manual/modules/ROOT/pages/apache-release/creating-a-release.adoc b/docs/hop-dev-manual/modules/ROOT/pages/apache-release/creating-a-release.adoc index df77bc52beb..048d1bd96d0 100644 --- a/docs/hop-dev-manual/modules/ROOT/pages/apache-release/creating-a-release.adoc +++ b/docs/hop-dev-manual/modules/ROOT/pages/apache-release/creating-a-release.adoc @@ -55,6 +55,37 @@ Build the software using the apache release profile mvn clean install -Papache-release ---- +=== Optional marketplace plugins + +Large / lesser-used plugins are *not* bundled in the hop-client zip (ASF package size). +Each plugin module with `src/assembly/assembly.xml` builds a standalone zip (Maven +assembly with `appendAssemblyId=false`). + +The root `maven-release-plugin` is configured with `-P=-assemblies -DskipTests`: + +* **`-assemblies`** — skip the `assemblies/` reactor (hop-client / hop-web products go to + dist.apache.org, not Maven Central). +* **Do not** disable the per-module **`assembly`** profile — that would drop plugin zips + from staging and break the marketplace. + +So `release:perform` publishes both the jar and the zip under the same GAV, for example: + +---- +https://repository.apache.org/content/groups/public/org/apache/hop/hop-tech-parquet//hop-tech-parquet-.zip +---- + +After staging a release candidate, verify marketplace zips in the Nexus staging +repository (see xref:apache-release/checking-a-release.adoc[Checking a release]). +Optional plugins are listed in +`plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml`. + +**SNAPSHOT** testing: Jenkins deploys plugin zips to the ASF snapshots repository so +marketplace can install them during development. Local-only testing can still use +`docker/marketplace-nexus/` and `publish-marketplace-plugins.sh`. + +Keep the client under the ASF ~850 MB package limit by running +`tools/check-assembly-size.sh` before promoting distribution artifacts. + we are going to use the `mvn release:prepare` command to prepare the release process full command: diff --git a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/index.adoc b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/index.adoc index 2174dff2bb4..5af4905ea6d 100644 --- a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/index.adoc +++ b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/index.adoc @@ -80,7 +80,7 @@ cp -r /tmp/hop-client/hop/lib/core/* $CATALINA_HOME/webapps/ROOT/WEB-INF/lib/ rm -f $CATALINA_HOME/webapps/ROOT/WEB-INF/lib/hop-ui-rcp-*.jar # Step 5: Copy beam libraries -cp -r /tmp/hop-client/hop/lib/beam/* $CATALINA_HOME/webapps/ROOT/WEB-INF/lib/ +cp -r /tmp/hop-client/hop/plugins/engines/beam/lib-beam/* $CATALINA_HOME/webapps/ROOT/WEB-INF/lib/ # Step 6: Copy plugins cp -r /tmp/hop-client/hop/plugins $CATALINA_HOME/ @@ -200,7 +200,7 @@ sed -i '' 's&lib/core/*&../../lib/*:WEB-INF/lib/*:lib/core/*&g' ${CATALINA_HOME} The `CLASSPATH` lines in the various scripts should look similar to the one below after editing: -`CLASSPATH="../../lib/*:WEB-INF/lib/*:lib/core/**:lib/beam/*:lib/swt/osx/arm64/*"` +`CLASSPATH="../../lib/*:WEB-INF/lib/*:lib/core/**:plugins/engines/beam/lib-beam/*:lib/swt/osx/arm64/*"` Once the classpaths have been updated, make sure to make the scripts executable: diff --git a/docs/hop-dev-manual/modules/ROOT/pages/marketplace-publish-plugin.adoc b/docs/hop-dev-manual/modules/ROOT/pages/marketplace-publish-plugin.adoc new file mode 100644 index 00000000000..c410953590a --- /dev/null +++ b/docs/hop-dev-manual/modules/ROOT/pages/marketplace-publish-plugin.adoc @@ -0,0 +1,484 @@ +//// +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. +//// +:description: How to package a third-party Hop plugin as a marketplace zip, declare Hop version and plugin dependencies, and publish releases to a private Nexus repository. +[[MarketplacePublishPlugin]] += Publishing a Hop plugin to your own Maven repository + +This guide is for plugin authors who develop *outside* the Apache Hop repository (for example `../hop-data-vault`) and want to distribute their plugin through the Hop *marketplace*: users install a Maven GAV zip into their Hop client’s `plugins/` tree. + +It walks through: + +* Packaging a marketplace-compatible zip +* Declaring which *Hop version* you require and which *other plugins* you depend on +* Deploying to a private Sonatype Nexus (or any Maven 2 layout repository) +* Automating a release for a *specific plugin version* with a script template + +Related reading: + +* xref:start-your-own-plugin.adoc[Creating your own plugin] — project skeleton and debugging +* xref:plugin-development.adoc[Plugins Development] — plugin types and the registry +* User manual: *Hop Marketplace* (`hop-tools/hop-marketplace.adoc`) — install, repos, hop-env apply/validate + +== Overview + +The marketplace downloads a zip from a Maven repository base URL using standard coordinates: + +[source,text] +---- +{repoBase}/{groupId with dots as /}/{artifactId}/{version}/{artifactId}-{version}.zip +---- + +Example private Nexus (fictional): + +[source,text] +---- +https://nexus.sandbox.example/repository/hop-plugins/ + com/example/hop-data-vault/1.2.0/hop-data-vault-1.2.0.zip +---- + +End users configure that base URL as a marketplace repository, then install: + +[source,bash] +---- +./hop marketplace repo add --id sandbox \ + --url https://nexus.sandbox.example/repository/hop-plugins/ +./hop marketplace install com.example:hop-data-vault:1.2.0 +# restart Hop +---- + +Or declare the same GAV in a `hop-env.yaml` and run `hop marketplace apply -f …`. + +TIP: Apache Hop’s own optional plugins use `groupId` `org.apache.hop` and are published via the ASF release process. Third-party plugins should use *your* groupId (for example `com.example`). + +== Prerequisites + +* Java 21 and Maven 3.6.3+ +* A Hop version to *compile against* (Maven property, for example `hop.version=2.19.0`) +* A Hop client for smoke-testing install (same major.minor line you support) +* A Maven repository (Sonatype Nexus OSS/Pro, Artifactory, etc.): + ** Hosted Maven repository (releases and/or snapshots) + ** Prefer *anonymous read* for day-to-day installs; use a deploy user only for publish +* Deploy credentials (`settings.xml` or `NEXUS_USER` / `NEXUS_PASSWORD`) + +For a local sandbox similar to Hop’s development stack, see `docker/marketplace-nexus/` in the Hop repository. Production teams usually run their own Nexus with corporate SSO and retention policies. + +== Project layout + +A typical standalone plugin project: + +[source,text] +---- +hop-data-vault/ + pom.xml # groupId / artifactId / version = YOUR plugin version + src/main/java/... + src/main/resources/ + version.xml # usually ${project.version} + src/assembly/assembly.xml # builds the installable zip + dependencies.xml # optional: other Hop plugins on disk (classloader) + scripts/release-to-nexus.sh # release automation (see below) + README.md # supported Hop versions, install steps +---- + +Start from the remote-plugin skeleton described in xref:start-your-own-plugin.adoc[] if you do not already have a project. + +=== Zip layout (critical) + +The zip must expand *into a Hop install root* so files land under `plugins/…`: + +[source,text] +---- +plugins/tech/data-vault/ + hop-data-vault-1.2.0.jar + version.xml + lib/ # third-party jars only (not hop-core/engine/ui) + dependencies.xml # optional +config/projects/samples/… # optional samples +---- + +Rules of thumb: + +* Use Maven assembly with `appendAssemblyId` *false* so the file is named `{artifactId}-{version}.zip` (marketplace expects that name on the repo path). +* Put the plugin jar and its *runtime* third-party libraries under the plugin directory (often `lib/`). +* *Do not* ship `hop-core`, `hop-engine`, or `hop-ui` jars — Hop already provides them. +* Exclude jars already present in Hop’s `lib/core` when possible to avoid duplicates and classloader conflicts. + +Minimal assembly sketch (standalone project; adjust paths and artifactId): + +[source,xml] +---- + + plugin + zip + . + + + + ${project.groupId}:${project.artifactId}:jar + + plugins/tech/data-vault + + + false + runtime + + org.apache.hop:* + + + plugins/tech/data-vault/lib + + + + + src/main/resources/version.xml + plugins/tech/data-vault + true + + + +---- + +Apache Hop in-tree plugins use a shared component (`assemblies/shared/hop-plugin-libs.xml`) and `${hop.plugin.libdir}`; third-party projects usually inline the layout as above. + +== Plugin version vs Hop version + +These are *different* numbers. Mixing them up is the most common packaging mistake. + +[cols="1,2,3"] +|=== +| Concept | Where you set it | What it means + +| *Plugin release version* +| Maven `project.version` (for example `1.2.0`) +| The version users install: `com.example:hop-data-vault:1.2.0` + +| *Hop compile version* +| Maven property `hop.version` (for example `2.19.0`) +| Hop APIs you compiled against (`hop-engine` *provided* dependency) + +| *Hop runtime expectation* +| README, release notes, sample hop-env `hopVersion` +| Which Hop clients you support at runtime +|=== + +=== Compile against Hop + +[source,xml] +---- + + 2.19.0 + + + + + org.apache.hop + hop-engine + ${hop.version} + provided + + + org.apache.hop + hop-core + ${hop.version} + provided + + + +---- + +Resolve Hop artifacts from Maven Central (released versions) or your mirror of `org.apache.hop`. + +=== Runtime compatibility (document it) + +Today the marketplace *does not hard-enforce* a minimum Hop version from the plugin zip. Authors should: + +. State supported Hop versions in the plugin README (for example “requires Hop 2.19.x”). +. Ship a sample hop-env that sets `hopVersion` to the Hop line you target (for optional Apache plugins installed alongside yours). +. Optionally add a small machine-readable file inside the plugin folder (convention for future tooling), for example: + +[source,json] +---- +{ + "groupId": "com.example", + "artifactId": "hop-data-vault", + "version": "1.2.0", + "hopVersionMin": "2.19.0", + "hopVersionMax": "2.19.999", + "requiresPlugins": [ + { "groupId": "org.apache.hop", "artifactId": "hop-tech-parquet", "version": "2.19.0" } + ] +} +---- + +Hop does not read this file yet; it is useful for your own installers and as a future marketplace extension. + +*Versioning advice:* + +* Treat Hop *minor* (2.19, 2.20) as an API line; re-test when users move minors. +* Bump your plugin *major* when you break APIs or require a new Hop minor. +* Keep a simple matrix in your README: + +[cols="1,1,1"] +|=== +| Plugin version | Built with Hop | Tested on Hop + +| 1.2.0 | 2.19.0 | 2.19.0–2.19.x +| 1.3.0 | 2.20.0 | 2.20.x +|=== + +== Other plugin dependencies + +There are three different “dependency” stories. + +=== Classpath dependencies between installed plugins (`dependencies.xml`) + +After install, Hop’s plugin classloader can add jars from *sibling plugin folders* using a `dependencies.xml` next to your plugin jar (same pattern as some Apache plugins such as Beam). + +[source,xml] +---- + + + + ../../tech/parquet + +---- + +Requirements: + +* The other plugin must already be installed under that path. +* Paths must match the real layout under the Hop install’s `plugins/` tree. +* This does *not* download anything; it only wires classloaders. + +=== Install-time dependencies (`hop-env.yaml`) + +Marketplace `apply` does *not* resolve a dependency graph. List every required plugin explicitly, *dependency plugins first*, then yours: + +[source,yaml] +---- +version: "1.0" +hopVersion: "2.19.0" +repositories: + - id: sandbox + url: "https://nexus.sandbox.example/repository/hop-plugins/" + - id: asf + url: "https://repository.apache.org/content/groups/public/" +plugins: + # Apache optional plugin your code needs on disk + - groupId: org.apache.hop + artifactId: hop-tech-parquet + version: "2.19.0" + # Your plugin + - groupId: com.example + artifactId: hop-data-vault + version: "1.2.0" +---- + +Document the same list in your README so users who install with a single `marketplace install` command know what else to install. + +=== Third-party Java libraries + +* Put them on the plugin classpath via assembly (`runtime` scope → plugin `lib/`). +* Prefer versions compatible with jars already in Hop `lib/core`. +* Check licenses before distributing binaries (https://www.apache.org/legal/resolved.html[ASF third-party policy] for Apache-hosted plugins; private plugins still need your own legal review). + +== Configure Maven and Nexus + +=== Server credentials + +In `~/.m2/settings.xml` (or CI secret injection): + +[source,xml] +---- + + + sandbox-hop-plugins + ${env.NEXUS_USER} + ${env.NEXUS_PASSWORD} + + +---- + +The `` must match `repositoryId` / `distributionManagement` below. + +=== distributionManagement (optional if you only use deploy-file) + +[source,xml] +---- + + + sandbox-hop-plugins + https://nexus.sandbox.example/repository/hop-plugins/ + + + sandbox-hop-plugins + https://nexus.sandbox.example/repository/hop-plugins/ + + +---- + +=== Deploy the zip + +After `mvn package` produces `target/hop-data-vault-1.2.0.zip`: + +[source,bash] +---- +mvn deploy:deploy-file \ + -DgroupId=com.example \ + -DartifactId=hop-data-vault \ + -Dversion=1.2.0 \ + -Dpackaging=zip \ + -Dfile=target/hop-data-vault-1.2.0.zip \ + -DrepositoryId=sandbox-hop-plugins \ + -Durl=https://nexus.sandbox.example/repository/hop-plugins/ \ + -DgeneratePom=true +---- + +If your Maven module is set up so the zip is attached with `appendAssemblyId=false` and packaging is wired for deploy, `mvn deploy` can replace deploy-file. + +Verify anonymous (or authenticated) download: + +[source,bash] +---- +curl -sI \ + "https://nexus.sandbox.example/repository/hop-plugins/com/example/hop-data-vault/1.2.0/hop-data-vault-1.2.0.zip" \ + | head -1 +# Expect HTTP 200 (or 401 only if you require auth for install) +---- + +IMPORTANT: Prefer *anonymous read* for marketplace installs. Wrong Basic auth credentials can cause HTTP 401 even when anonymous would work. Deploy credentials should stay in CI, not in hop-config for every user. + +== Release script (specific plugin version) + +A copyable template lives in the Hop repository: + +`docs/hop-dev-manual/modules/ROOT/examples/marketplace/release-plugin-to-nexus.sh` + +Copy it into your plugin repo as `scripts/release-to-nexus.sh` and adjust defaults (`GROUP_ID`, `ARTIFACT_ID`, paths). + +=== What the script does + +. Requires a *plugin version* argument (for example `1.2.0`). +. Optional `--hop-version 2.19.0` (compile/test against that Hop line). +. Optional Nexus URL / repository id; credentials from env or Maven settings. +. Runs `mvn clean verify` with `-Dhop.version=…`. +. Packages the assembly zip. +. Deploys the zip with `deploy:deploy-file`. +. Prints end-user install commands. +. Supports `--dry-run` and optional `git tag`. + +=== Example + +[source,bash] +---- +export NEXUS_USER=ci-deploy +export NEXUS_PASSWORD='…' +export NEXUS_URL=https://nexus.sandbox.example/repository/hop-plugins/ + +./scripts/release-to-nexus.sh 1.2.0 --hop-version 2.19.0 + +# SNAPSHOT to the same (or snapshot) repo: +./scripts/release-to-nexus.sh 1.3.0-SNAPSHOT --hop-version 2.19.0-SNAPSHOT +---- + +=== SNAPSHOT vs release + +* *Release* versions (`1.2.0`) should be immutable — do not redeploy over them. +* *SNAPSHOT* versions can be redeployed; Nexus stores unique timestamps. Marketplace resolves SNAPSHOTs via `maven-metadata.xml` when needed. +* Use separate Nexus repos or policies for snapshots vs releases if your ops team requires it. + +== End-user install on a sandbox Hop + +. Ensure Hop is the supported line (for example 2.19.x). +. Add your repository (CLI or hop-config): + +[source,bash] +---- +./hop marketplace repo add --id sandbox \ + --url https://nexus.sandbox.example/repository/hop-plugins/ +# optional: make it primary if ASF/Central should not be tried first +./hop marketplace repo set-primary sandbox +---- + +. Install: + +[source,bash] +---- +./hop marketplace install com.example:hop-data-vault:1.2.0 +# or apply a hop-env that lists dependencies + your plugin +./hop marketplace apply -f hop-env-data-vault.yaml +---- + +. Restart Hop so the plugin registry reloads. + +. Smoke-test: open a pipeline that uses your transform/action; check logs for classloader errors. + +=== Troubleshooting + +[cols="1,2"] +|=== +| Symptom | Likely cause + +| HTTP 401 / 403 +| Repo requires auth, or *wrong* Basic auth is set (clear `HOP_MARKETPLACE_*` for anonymous repos) + +| HTTP 404 +| Wrong groupId/artifactId/version, or zip not named `{artifactId}-{version}.zip` + +| Install OK but plugin missing in GUI +| Zip layout not under `plugins/…`, or restart not done + +| `NoClassDefFoundError` for another plugin +| Missing `dependencies.xml` peer or peer not installed; fix hop-env order + +| API / method mismatch errors +| Plugin built against a different Hop line than the running client +|=== + +== Author checklist + +* [ ] `hop.version` set for compile (`provided` hop-engine / hop-core) +* [ ] Plugin `version` chosen for this release +* [ ] Assembly zip expands under `plugins/…` with correct jar + `version.xml` +* [ ] No hop-core / hop-engine / hop-ui jars inside the zip +* [ ] Third-party libs reviewed (size, license, overlap with Hop) +* [ ] Other plugins documented (`dependencies.xml` and/or hop-env list) +* [ ] Nexus allows read for installs; deploy credentials only for CI +* [ ] Smoke install into a clean Hop client matching the supported line +* [ ] Release script run for this version; git tag if you use tags +* [ ] README updated with install commands and Hop compatibility matrix + +== Limitations (current Hop marketplace) + +[cols="1,2,2"] +|=== +| Need | Today | Possible later + +| Minimum Hop version enforcement +| Document in README / hop-env only +| Marketplace reads author metadata from the zip + +| Automatic install of dependent plugins +| List them explicitly in hop-env / docs +| Dependency graph resolution + +| Catalog search for private plugins +| Users install by GAV or private `query` if you add to a registry +| Configurable search API against your Nexus + +| Signing +| Optional; use Nexus checksums +| Document signing workflow if required by your org +|=== diff --git a/docs/hop-dev-manual/modules/ROOT/pages/start-your-own-plugin.adoc b/docs/hop-dev-manual/modules/ROOT/pages/start-your-own-plugin.adoc index c51e0bd94fb..2d2d6c7eec5 100644 --- a/docs/hop-dev-manual/modules/ROOT/pages/start-your-own-plugin.adoc +++ b/docs/hop-dev-manual/modules/ROOT/pages/start-your-own-plugin.adoc @@ -22,6 +22,8 @@ under the License. This guide is to help you set up and start development on a plugin that will not be included in the Hop repository. For more information on plugin types and how to add them to the Hop repository check out xref:plugin-development.adoc[following guide]. +To package a marketplace zip and publish releases to your own Nexus (including Hop version requirements and plugin dependencies), see xref:marketplace-publish-plugin.adoc[Publishing a Hop plugin to your own Maven repository]. + The Apache software foundation has a strict policy on source licenses including all dependencies used to provide certain functionality. To see which dependencies are allowed in an Apache project check https://www.apache.org/legal/resolved.html[following page]. If your plugin consists of code and dependencies in category A it can safely be added to the Hop repository and community can help maintain the code. diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-environment.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-environment.png new file mode 100644 index 00000000000..3dd4005ce2d Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-environment.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-plugins.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-plugins.png new file mode 100644 index 00000000000..e9f6341577c Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-plugins.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repositories.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repositories.png new file mode 100644 index 00000000000..5d4e4602199 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repositories.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor-plugins.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor-plugins.png new file mode 100644 index 00000000000..3709eb5c201 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor-plugins.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor.png new file mode 100644 index 00000000000..52c4e2804fc Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-dialog-repository-editor.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-environment-drift.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-environment-drift.png new file mode 100644 index 00000000000..c531ed1a024 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-environment-drift.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-hopgui-toolbar-icon.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-hopgui-toolbar-icon.png new file mode 100644 index 00000000000..e082a32440d Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-hopgui-toolbar-icon.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-lifecycle-enviromnent-enforce-configuration.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-lifecycle-enviromnent-enforce-configuration.png new file mode 100644 index 00000000000..8388b94cd6f Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-lifecycle-enviromnent-enforce-configuration.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-welcome-dialog.png b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-welcome-dialog.png new file mode 100644 index 00000000000..33b4e010f91 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/marketplace/marketplace-welcome-dialog.png differ diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc b/docs/hop-user-manual/modules/ROOT/nav.adoc index 86fb13bc8e9..2ecf6c5a318 100644 --- a/docs/hop-user-manual/modules/ROOT/nav.adoc +++ b/docs/hop-user-manual/modules/ROOT/nav.adoc @@ -511,6 +511,7 @@ under the License. ** xref:hop-server/async-web-service.adoc[Async Web Service] * xref:hop-rest/index.adoc[Hop REST Web Application] * xref:hop-tools/index.adoc[Hop Tools] +** xref:hop-tools/hop-marketplace.adoc[hop marketplace] ** xref:hop-tools/hop-conf/hop-conf.adoc[hop-conf] ** xref:hop-tools/hop-encrypt.adoc[hop-encrypt] ** xref:hop-tools/hop-gui.adoc[hop-gui] diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc index af0496cc254..7dd11e2612e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-conf/hop-conf.adoc @@ -16,7 +16,7 @@ under the License. //// :openvar: ${ :closevar: } -:description: Hop Conf is a command line tool to manage environments. The hop-conf.sh script can be run with the -h flag (`./hop-conf.sh -h`) to display available options. +:description: Hop Conf is a command line tool to manage your Hop client installation = Hop Conf - The Hop command line configuration tool diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc new file mode 100644 index 00000000000..f80049a632b --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/hop-marketplace.adoc @@ -0,0 +1,447 @@ +//// +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. +//// +:description: Install optional Hop plugins from Apache Repository / Maven Central +:openvar: ${ +:closevar: } + +:TOC: + += Hop Marketplace + +Some Hop plugins are optional: they are not bundled in the default client zip so the download sizes stay within reason. The *marketplace* installs those plugins into the running Hop client’s `plugins/` tree from a Maven repository. + +== Restore the full plugin set + +The default hop-client zip is intentionally smaller: we made large plugins optional since version 2.19.0. To install +*all* marketplace-optional plugins that used to ship in the download (before version 2.19), +use the generated environment file at the installation root: + +[source,bash] +---- +cd /path/to/hop +./hop marketplace apply -f full-client-env.yaml + +# Please restart Hop server or GUI instances that might still be open +---- + +*Notes*: + +* Requires network access to Apache Repository / Maven Central (or a mirror configured in hop-config). +* For SNAPSHOT builds when developing you can point the marketplace at a local Nexus that has the plugin zip files published. See folder `docker/marketplace-nexus` in the source code to run your own Nexus locally. +* The download is large (hundreds of MB); that is expected. +* Do *NOT* pass `--prune` unless you also want other marketplace installs removed. +* `full-client-env.yaml` is generated at build time from marketplace plugin source file `optional-plugins.yaml` (source of truth). + +Beam SDKs/runners live under `plugins/engines/beam/lib-beam` (installed with the Beam plugin), not under `lib/beam`. Launch scripts add that folder to the classpath only when it exists. Developers who want Beam baked into a local hop-client build can use Maven profile `-Pbeam` (for example `./mvnw -pl assemblies/client -am package -Pbeam`). + +== Defaults + +Without extra configuration, Hop uses: + +. *asf* (primary) — `https://repository.apache.org/content/groups/public/` +. *central* (fallback) — `https://repo1.maven.org/maven2/` + +Install tries the primary repository first, then other *enabled* repositories, until the plugin zip is found. + +Settings are stored under the `marketplace` key in + +[source] +---- +${HOP_CONFIG_FOLDER}/hop-config.json +---- + +[NOTE] +`config/hop-config.json` in your client distribution is only provided to get started but will be not survive a Hop version upgrade. Configure your own configuration. + +== CLI overview + +From the Hop install directory: + +[source,bash] +---- +./hop marketplace install hop-tech-parquet +./hop marketplace install hop-engines-spark:2.19.0 +./hop marketplace install hop-engines-spark:2.19.0 --repo asf +./hop marketplace list +./hop marketplace query parquet +./hop marketplace query "spark engine" +./hop marketplace uninstall hop-tech-parquet + +# Repositories +./hop marketplace repo list +./hop marketplace repo add --id corporate --url https://nexus.example.com/repository/hop/ --primary +./hop marketplace repo set-primary asf +./hop marketplace repo reset-defaults + +# Declarative environment (CI / Docker) +./hop marketplace apply -f hop-env.yaml +./hop marketplace apply -f hop-env.yaml --prune +./hop marketplace validate -f hop-env.yaml +./hop marketplace validate -f hop-env.yaml --strict +---- + +After install, apply, or uninstall, *restart Hop* so the plugin registry reloads. + +Coordinates for `install`: `artifactId`, `artifactId:version`, or `groupId:artifactId:version`. +Optional `--repo ` forces a single repository (skips the fallback chain). + +`query` searches the local optional-plugin catalog (not Maven Central). The filter is a +case-insensitive substring matched against artifact id, name, category, description, and install +path. Omit the filter (or pass an empty string) to list every optional plugin. Matches may show +`[installed]` when the plugin folder is present under the Hop install. + +== GUI + +The marketplace GUI is the primary way to install optional plugins, manage Maven repositories, and edit declarative environment files without using the CLI. + +=== Opening the marketplace + +You can open the marketplace dialog from: + +* *Tools → Marketplace…* +* The Hop Gui toolbar marketplace icon + +image::marketplace/marketplace-hopgui-toolbar-icon.png[Hop Gui toolbar marketplace icon,50%,align="left"] + +On first use you may also see the welcome page that introduces the marketplace: + +image::marketplace/marketplace-welcome-dialog.png[Marketplace welcome dialog,75%,align="left"] + +The main *Plugin Marketplace* dialog has three tabs: + +* *Plugins* — browse, search, install, and uninstall optional plugins +* *Environment* — edit `hop-env.yaml` / JSON environments and validate or apply them +* *Repositories* — configure Maven repositories (same settings as the CLI / hop-config) + +After install, apply, or uninstall, *restart Hop* so the plugin registry reloads. + +=== Plugins tab + +image::marketplace/marketplace-dialog-plugins.png[Marketplace dialog Plugins tab,75%,align="left"] + +Use this tab to work with the local optional-plugin catalog: + +* *Search* — type three or more characters to filter by name, artifact id, category, description, or install path. Clear the filter with the clear button. +* Table columns: *Name*, *Artifact*, *Category*, *Status*, *Description*. +* *Status* values: +** *Installed* — present on disk with a marketplace install receipt (version shown when known) +** *Present on disk* — plugin folder exists but no marketplace receipt +** *Not installed* — not found under `plugins/` +* *Install* / *Uninstall* / *Refresh* — act on the selected plugin (uninstall requires a marketplace receipt) + +Install uses the *primary* repository first, then other *enabled* repositories (see <>). + +=== Environment tab + +image::marketplace/marketplace-dialog-environment.png[Marketplace dialog Environment tab,75%,align="left"] + +This tab embeds the full environment editor (no separate “Edit…” window). It is the GUI equivalent of `hop marketplace validate` / `apply` and of editing `hop-env.yaml` by hand. + +==== File toolbar + +* *New* — empty environment specification +* *Open…* — open a `hop-env.yaml` / `.yml` / `.json` file +* *Save* / *Save As…* — write YAML or JSON (by extension) +* Filename *combo* — current path plus recently used environment files (remembered via the Hop audit list for this namespace). Selecting an entry loads that file. +* On open, Hop prefers the last file you edited in this dialog; otherwise it may pre-select `full-client-env.yaml` at the install root when that file exists. + +==== Nested editor tabs + +[cols="1,3"] +|=== +| Tab | Contents + +| *General* +| Spec `version`, `hopVersion`, and `enforceOnRun` (when true, hop-run can fail if the install drifts from this file; also controlled by `-Dhop.env.enforce=true`). + +| *Repositories* +| Ordered list of Maven repositories for *this environment file* (first row is primary). Optional username/password. *Import from hop-config…* copies enabled repositories from hop-config (one-shot; env-file repos stay independent of the main *Repositories* tab). + +| *Plugins* +| Plugin GAVs to install. *From catalog…* multi-selects from the optional-plugin catalog. Leave Group ID blank to use `org.apache.hop`. Version can default from *Hop version* on General when applying. + +| *Dependencies* +| Optional jars (for example JDBC) with target directory under Hop home (default `lib/jdbc`). +|=== + +==== Validate and Apply + +At the bottom of the Environment tab: + +* *Validate* — compare the local install to the saved environment file without downloading. +* *Apply* — install missing plugins and dependencies declared in the file. +* *Prune extras on apply* — same as CLI `apply --prune` (confirm before running). +* *Strict validate* — also fail when extra marketplace-installed plugins (receipts under `plugins/.marketplace/`) are not listed in the file. + +If validation finds drift, Hop shows a report. Use *Apply* to fix, then restart Hop. + +image::marketplace/marketplace-environment-drift.png[Marketplace environment drift report,75%,align="left"] + +[[marketplace-gui-repositories]] +=== Repositories tab + +image::marketplace/marketplace-dialog-repositories.png[Marketplace dialog Repositories tab,75%,align="left"] + +Configure the global marketplace repository list (stored under `marketplace` in `${HOP_CONFIG_FOLDER}/hop-config.json` — the same config the CLI uses): + +* Table columns: *Primary*, *Enabled*, *Id*, *Name*, *URL*, *Auth* +* *Add* / *Edit* / *Remove* — maintain repository entries +* *Set primary* — install tries this repository first +* *Up* / *Down* — order fallbacks after the primary +* *Reset defaults* — Apache Repository (primary) + Maven Central +* *Save* — write hop-config (required to keep changes) + +Add or edit a repository with id, name, URL, optional credentials, and primary/enabled flags: + +image::marketplace/marketplace-dialog-repository-editor.png[Add or edit marketplace repository,60%,align="left"] + +=== Projects lifecycle environments + +When the *Projects* plugin is installed, each lifecycle environment can store marketplace-specific *attributes* (namespaced under the `marketplace` group). On the environment properties dialog, open the *Marketplace plugins* tab: + +image::marketplace/marketplace-lifecycle-enviromnent-enforce-configuration.png[Lifecycle environment Marketplace plugins configuration,75%,align="left"] + +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 (use only on controlled machines) + +If *On enable* is unset, purpose-based defaults apply: *Production* → enforce, *Testing* / *Acceptance* → warn, *Development* / CI-style purposes → off. + +This complements hop-env `enforceOnRun` (checked at hop-run) and the Environment tab *Validate* / *Apply* actions (manual checks from the marketplace dialog). + +== Declarative environments (`hop-env.yaml`) + +For CI/CD, Docker, and reproducible servers you can declare the required plugins (and optional jars) in a file, commit it to Git, and apply it without using the GUI. + +Supported file names / formats: + +* `hop-env.yaml` / `hop-env.yml` (YAML) +* `hop-env.json` (JSON — same field names) + +=== `hop marketplace apply` + +Installs missing plugins and dependencies declared in the environment file. Already-satisfied plugins are skipped. + +[cols="1,3"] +|=== +| Option | Description + +| `-f`, `--file` *(required)* +| Path to `hop-env.yaml` or `hop-env.json`. + +| `--prune` +| After installing what the file requires, *uninstall* marketplace-managed plugins that have an install receipt but are *not* listed under `plugins`. Core/bundled plugins and plugins present on disk without a marketplace receipt are not removed by prune. +|=== + +Examples: + +[source,bash] +---- +# Install everything listed (idempotent) +./hop marketplace apply -f hop-env.yaml + +# Also remove marketplace plugins that are no longer in the file +./hop marketplace apply -f ./deploy/hop-env.yaml --prune +---- + +Behaviour notes: + +* Repositories listed in the file override hop-config for this apply (first entry is primary; later entries are fallbacks). If the file omits `repositories`, hop-config repositories are used. +* Plugin `version` defaults to `hopVersion` in the file, then the marketplace/hop version. +* Plugins already on disk without a marketplace receipt (for example installed offline with `tools/install-wave1-plugins.sh`) count as satisfied and are not re-downloaded. +* Dependencies are downloaded as jars into `target` (default `lib/jdbc`). + +Always restart Hop after a successful apply when new plugins were installed. + +=== `hop marketplace validate` + +Compares the local install to the environment file *without* downloading or installing anything. Useful in CI as a pre-flight check. + +[cols="1,3"] +|=== +| Option | Description + +| `-f`, `--file` +| Path to the environment file. If omitted, Hop discovers a file (see <>). + +| `--strict` +| Also fail when *extra* marketplace-installed plugins (receipts under `plugins/.marketplace/`) are present that are not listed in the file. Without `--strict`, extras are ignored. +|=== + +Exit codes: + +* *0* — environment matches (prints `OK: environment matches …`) +* *non-zero* — drift detected or error (prints a drift report and suggests `hop marketplace apply -f …`) + +What counts as drift (always hard-fail): + +* *Missing plugin* — not on disk and no install receipt +* *Version mismatch* — receipt version differs from the version required in the file +* *Missing dependency* — jar not found under the dependency `target` directory + +Examples: + +[source,bash] +---- +./hop marketplace validate -f hop-env.yaml +./hop marketplace validate -f hop-env.yaml --strict + +# Discover hop-env under PROJECT_HOME or the Hop install +export PROJECT_HOME=/path/to/my-project +./hop marketplace validate +---- + +=== hop-run enforcement + +When a hop-env file is found and enforcement is on, `hop-run` validates before execution and aborts on missing plugins/deps or version mismatches (same checks as validate without `--strict`). + +Enable enforcement either: + +* in the file: `enforceOnRun: true`, or +* with a system property: `-Dhop.env.enforce=true` (or `Y`) + +If a hop-env file is present but `enforceOnRun` is false and the property is unset, hop-run only logs that the file was found and skips the check. + +[[hop-env-discovery]] +=== How the environment file is discovered + +Used by `validate` (when `-f` is omitted) and by hop-run: + +. Explicit path: `-f` / `--file` (validate only), or variable / property `HOP_ENV_FILE` / `hop.env.file` +. Under `PROJECT_HOME` (system property or env): `hop-env.yaml`, then `.yml`, then `.json` +. Under the Hop install root: same file names + +=== `hop-env.yaml` format + +Full example: + +[source,yaml] +---- +# Schema version of this file format (optional; default "1.0") +version: "1.0" + +# Default plugin version when a plugin entry omits "version" +hopVersion: "2.19.0" + +# If true, hop-run fails when the install drifts from this file +enforceOnRun: false + +# Maven repositories for apply (optional). +# First entry is primary; later entries are fallbacks for that apply only. +# If omitted, hop-config marketplace.repositories are used. +repositories: + - id: asf + url: "https://repository.apache.org/content/groups/public/" + - id: central + url: "https://repo1.maven.org/maven2/" + # Private mirror example: + # - id: corporate + # url: "https://nexus.example.com/repository/hop-plugins/" + # username: reader + # password: "…" # prefer env HOP_MARKETPLACE_PASSWORD for secrets + +# Required optional plugins (installed into plugins/) +plugins: + - artifactId: hop-tech-parquet + version: "2.19.0" # optional if hopVersion is set + - groupId: org.apache.hop # optional; default org.apache.hop + artifactId: hop-engines-spark + version: "2.19.0" + # Beam engine plugin only (Beam SDKs ship under plugins/engines/beam/lib-beam with the plugin): + # - artifactId: hop-engines-beam + # version: "2.19.0" + +# Optional non-plugin jars (e.g. JDBC drivers) +dependencies: + - groupId: org.postgresql + artifactId: postgresql + version: "42.7.3" + target: lib/jdbc # optional; default lib/jdbc +---- + +==== Top-level fields + +[cols="1,1,3"] +|=== +| Field | Required | Description + +| `version` +| No +| Format version of the hop-env file itself (default `1.0`). + +| `hopVersion` +| Recommended +| Default version for plugin entries that omit `version`. Also used when aligning plugins with a Hop release. + +| `enforceOnRun` +| No +| When `true`, hop-run validates this file before running and fails on missing plugins/deps or version mismatches. + +| `repositories` +| No +| List of Maven base URLs used when *applying* this file. Order: first = primary, rest = fallbacks. Each item: `id`, `url`, optional `username` / `password`. + +| `plugins` +| No +| List of optional Hop plugins to install. Each item: `artifactId` (required), optional `groupId` (default `org.apache.hop`), optional `version` (default `hopVersion` / Hop version). + +| `dependencies` +| No +| Extra jars (not Hop plugin zips). Each item: `groupId`, `artifactId`, `version` (all required), optional `target` directory under the Hop install (default `lib/jdbc`). Downloaded as `artifactId-version.jar`. +|=== + +==== Minimal examples + +*Only plugins, ASF defaults from hop-config:* + +[source,yaml] +---- +version: "1.0" +hopVersion: "2.19.0" +plugins: + - artifactId: hop-tech-parquet + - artifactId: hop-tech-cassandra +---- + +*CI gate (no install):* + +[source,bash] +---- +./hop marketplace validate -f hop-env.yaml --strict || exit 1 +---- + +*Docker / pipeline install then run:* + +[source,bash] +---- +./hop marketplace apply -f /files/hop-env.yaml +# restart container or new process so plugins load +./hop-run.sh -f /files/main.hwf +---- + +A longer sample ships with the marketplace plugin sources (`plugins/misc/marketplace/src/main/samples/hop-env.example.yaml`) and may appear under samples after the plugin is installed. + +== Enterprise / air-gapped + +Point marketplace repositories at your internal Nexus or Artifactory mirror of `org.apache.hop` plugin zips. Prefer anonymous read where possible; for private repos set Basic auth on the repository entry (in hop-config or in hop-env `repositories`) or use `HOP_MARKETPLACE_USERNAME` / `HOP_MARKETPLACE_PASSWORD`. Wrong credentials can cause HTTP 401 even when anonymous access would work. + +== For plugin authors + +If you develop a third-party plugin (outside the Hop repository) and want to publish installable zips to your own Nexus, see the developer guide *Publishing a Hop plugin to your own Maven repository* in the Hop Development Manual (`marketplace-publish-plugin.adoc`). It covers packaging, Hop version requirements, other plugin dependencies, and a sample release script. diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/index.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/index.adoc index 58d917c38aa..8e49c6561d3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/hop-tools/index.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/hop-tools/index.adoc @@ -22,6 +22,7 @@ Even though Hop developers will spend the majority of their time in Hop Gui, Hop This page provides an overview of the tools that are available in the platform. * xref:hop-tools/hop.adoc[Hop] +* xref:hop-tools/hop-marketplace.adoc[Hop Marketplace] * xref:hop-tools/hop-conf/hop-conf.adoc[Hop Conf] * xref:hop-tools/hop-encrypt.adoc[Hop Encrypt] * xref:hop-tools/hop-gui.adoc[Hop Gui] diff --git a/integration-tests/scripts/run-tests-docker.sh b/integration-tests/scripts/run-tests-docker.sh index f6ebeac88bf..8d70ef94bc0 100755 --- a/integration-tests/scripts/run-tests-docker.sh +++ b/integration-tests/scripts/run-tests-docker.sh @@ -275,6 +275,16 @@ else echo "Skipping client unzip (CLIENT_UNZIP=${CLIENT_UNZIP}, using existing ${HOP_DIR})" fi +# Optional plugins (Wave 1) are not in hop-client.zip; install from reactor zips for ITs. +if [ -x "${REPO_ROOT}/tools/install-wave1-plugins.sh" ]; then + echo "Installing Wave 1 marketplace plugins into ${HOP_DIR} for integration tests" + "${REPO_ROOT}/tools/install-wave1-plugins.sh" "${HOP_DIR}" || { + echo "WARNING: install-wave1-plugins.sh reported errors; some ITs may fail if plugins are missing" + } +else + echo "WARNING: tools/install-wave1-plugins.sh not found; optional plugins not installed into ${HOP_DIR}" +fi + # Versioned Spark client packs are not in the client zip. Re-materialise after unzip so # HOP_SPARK_CLIENT_VERSION=… finds lib/spark-clients// (includes spark-streaming, etc.). # Also copy the selected pack into lib/spark-client/ so the default driver classpath always @@ -324,74 +334,108 @@ docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml build --build- # project that actually references the fat jar is about to run. BEAM_IMAGE_BUILT="false" +write_surefire_skipped() { + local name="$1" + local reason="$2" + local report="${CURRENT_DIR}/../surefire-reports/surefile_${name}.xml" + mkdir -p "${CURRENT_DIR}/../surefire-reports" + cat >"${report}" < + + + +EOF +} + +write_surefire_env_failure() { + local name="$1" + local detail="$2" + local report="${CURRENT_DIR}/../surefire-reports/surefile_${name}.xml" + mkdir -p "${CURRENT_DIR}/../surefire-reports" + cat >"${report}" < + +${detail} + +EOF +} + # Loop over project folders for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do + if [[ "$d" == *"scripts/" ]] || [[ "$d" == *"surefire-reports/" ]] || [[ "$d" == *"hopweb/" ]]; then + continue + fi - if [[ "$d" != *"scripts/" ]] && [[ "$d" != *"surefire-reports/" ]] && [[ "$d" != *"hopweb/" ]]; then - # If there is a file called disabled.txt the project is disabled - if [ ! -f "$d/disabled.txt" ]; then - - PROJECT_NAME=$(basename $d) + # Normalize project name from the folder we are iterating + PROJECT_NAME=$(basename "${d}") - echo "Project name: ${PROJECT_NAME}" - echo "project path: $d" - echo "docker compose path: ${DOCKER_FILES_DIR}" + # If there is a file called disabled.txt the project is disabled — do not pretend Docker failed + if [ -f "$d/disabled.txt" ]; then + echo "Project ${PROJECT_NAME} is disabled (disabled.txt present); skipping." + if [ "${SUREFIRE_REPORT}" = "true" ]; then + write_surefire_skipped "${PROJECT_NAME}" "Project disabled via disabled.txt" + fi + continue + fi - # If this project references the Hop fat jar (Beam runners), make sure hop-beam-image exists. - # Built once per run, and only when such a project is actually enabled. - if [ "${BEAM_IMAGE_BUILT}" != "true" ] && grep -rqs "hop-fatjar.jar" "$d" 2>/dev/null; then - echo "Project ${PROJECT_NAME} needs the Hop fat jar; building hop-beam-image (once)." - if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then - echo "Spark client pack for fat jar: ${HOP_SPARK_CLIENT_VERSION}" - fi - HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}" \ - docker compose -f ${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml build \ - --build-arg HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}" - EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" "${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml") - BEAM_IMAGE_BUILT="true" - fi + echo "Project name: ${PROJECT_NAME}" + echo "project path: $d" + echo "docker compose path: ${DOCKER_FILES_DIR}" - # Check if specific compose exists + # If this project references the Hop fat jar (Beam runners), make sure hop-beam-image exists. + # Built once per run, and only when such a project is actually enabled. + if [ "${BEAM_IMAGE_BUILT}" != "true" ] && grep -rqs "hop-fatjar.jar" "$d" 2>/dev/null; then + echo "Project ${PROJECT_NAME} needs the Hop fat jar; building hop-beam-image (once)." + if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then + echo "Spark client pack for fat jar: ${HOP_SPARK_CLIENT_VERSION}" + fi + HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}" \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml build \ + --build-arg HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}" + EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" "${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml") + BEAM_IMAGE_BUILT="true" + fi - if [ -n "${TEST_FILTER}" ]; then - echo "TEST_FILTER: ${TEST_FILTER}" - fi + if [ -n "${TEST_FILTER}" ]; then + echo "TEST_FILTER: ${TEST_FILTER}" + fi - if [ -f "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml" ]; then - echo "Project compose exists." - EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml") - # Rebuild project images so SPARK_VERSION (and similar) build args take effect. - # hop_server also must rebuild: its hop-server service image (apache/hop:Development - # from docker/Dockerfile) otherwise stays cached and can miss client-side assembly - # plugins needed by remote-export ITs (main-0008/0009/0010). - if [ "${PROJECT_NAME}" = "spark" ]; then - echo "Spark IT cluster version: ${SPARK_VERSION} (hadoop ${HADOOP_VERSION})" - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} SPARK_VERSION=${SPARK_VERSION} HADOOP_VERSION=${HADOOP_VERSION} SPARK_BASE_URL=${SPARK_BASE_URL} \ - docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit - elif [ "${PROJECT_NAME}" = "hop_server" ]; then - echo "Rebuilding hop_server images so remote Hop Server matches current assemblies" - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ - docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit - else - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ - docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --abort-on-container-exit - fi - else - echo "Project compose does not exists." - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ - docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml up --abort-on-container-exit - fi + COMPOSE_EXIT=0 + if [ -f "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml" ]; then + echo "Project compose exists." + EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml") + # Rebuild project images so SPARK_VERSION (and similar) build args take effect. + # hop_server also must rebuild: its hop-server service image (apache/hop:Development + # from docker/Dockerfile) otherwise stays cached and can miss client-side assembly + # plugins needed by remote-export ITs (main-0008/0009/0010). + if [ "${PROJECT_NAME}" = "spark" ]; then + echo "Spark IT cluster version: ${SPARK_VERSION} (hadoop ${HADOOP_VERSION})" + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} SPARK_VERSION=${SPARK_VERSION} HADOOP_VERSION=${HADOOP_VERSION} SPARK_BASE_URL=${SPARK_BASE_URL} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit \ + || COMPOSE_EXIT=$? + elif [ "${PROJECT_NAME}" = "hop_server" ]; then + echo "Rebuilding hop_server images so remote Hop Server matches current assemblies" + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit \ + || COMPOSE_EXIT=$? + else + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --abort-on-container-exit \ + || COMPOSE_EXIT=$? fi + else + echo "Project compose does not exists." + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml up --abort-on-container-exit \ + || COMPOSE_EXIT=$? fi - # Create final report + # Create final report only when the project was actually run and no report was produced if [ "${SUREFIRE_REPORT}" = "true" ]; then if [ ! -f "${CURRENT_DIR}/../surefire-reports/surefile_${PROJECT_NAME}.xml" ]; then - echo "" >"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml - echo "" >>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml - echo "" >>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml - echo "" >>"${CURRENT_DIR}"/../surefire-reports/surefile_${PROJECT_NAME}.xml + write_surefire_env_failure "${PROJECT_NAME}" \ + "Could not start docker environment for ${PROJECT_NAME} (compose exit ${COMPOSE_EXIT}). Check docker compose logs above." fi fi done diff --git a/plugins/engines/beam/pom.xml b/plugins/engines/beam/pom.xml index 13714cfa613..e13686c5270 100644 --- a/plugins/engines/beam/pom.xml +++ b/plugins/engines/beam/pom.xml @@ -1659,4 +1659,48 @@ + + + + + + maven-assembly-plugin + + + assembly-beam-libs + + single + + package + + true + + ${project.basedir}/src/assembly/assembly-libs.xml + + + + + + assembly_package + + single + + package + + false + + ${project.basedir}/src/assembly/assembly.xml + + + + + + + + diff --git a/plugins/engines/beam/src/assembly/assembly-beam-runtime.xml b/plugins/engines/beam/src/assembly/assembly-beam-runtime.xml new file mode 100644 index 00000000000..369c7ca8325 --- /dev/null +++ b/plugins/engines/beam/src/assembly/assembly-beam-runtime.xml @@ -0,0 +1,154 @@ + + + + + + runtime + + org.apache.hop:hop-engines-beam:jar + org.apache.hadoop:*:jar + org.apache.hadoop.thirdparty:*:jar + org.yaml:snakeyaml:jar + com.google.protobuf:protobuf-java:jar + com.google.protobuf:protobuf-java-util:jar + org.jetbrains:annotations:jar + com.google.auto.value:auto-value-annotations:jar + org.apache.beam:beam-vendor-guava-32_1_2-jre:jar + com.github.luben:zstd-jni:jar + com.google.auth:google-auth-library-oauth2-http:jar + com.google.oauth-client:google-oauth-client:jar + com.google.api-client:google-api-client:jar + com.google.http-client:google-http-client:jar + com.google.http-client:google-http-client-apache-v2:jar + com.google.http-client:google-http-client-appengine:jar + google-http-client-gson:jar + com.google.http-client:google-http-client-jackson2:jar + com.google.android:annotations:jar + org.codehaus.mojo:animal-sniffer-annotations:jar + com.google.api:api-common:jar + com.google.auto.value:auto-value-annotations:jar + com.google.guava::jar + org.jetbrains.kotlin::jar + io.netty::jar + org.codehaus.mojo:animal-sniffer-annotations:jar + io.projectreactor.netty::jar + io.grpc:grpc-netty-shaded:jar + io.grpc:grpc-xds:jar + org.apache.kafka::jar + org.xerial.snappy:snappy-java:jar + org.eclipse.jetty::jar + org.eclipse.jetty.compression::jar + com.google.cloud:google-cloud-storage:jar + io.grpc::jar + com.google.cloud:google-cloud-core-grpc:jar + com.google.api.grpc:grpc-google-cloud-storage-v2:jar + com.google.api:gax-grpc:jar + com.google.api.grpc::jar + org.checkerframework:checker-qual:jar + io.confluent:common-utils:jar + commons-cli:commons-cli:jar + commons-codec:commons-codec:jar + commons-logging:commons-logging:jar + org.apache.commons:commons-text:jar + com.google.cloud.opentelemetry:detector-resources-support:jar + com.google.errorprone:error_prone_annotations:jar + com.google.cloud.opentelemetry:exporter-metrics:jar + com.google.api:gax:jar + com.google.api:gax-httpjson:jar + com.google.apis:google-api-services-storage:jar + com.google.auth:google-auth-library-credentials:jar + com.google.auth:google-auth-library-oauth2-http:jar + com.google.cloud:google-cloud-core:jar + com.google.cloud:google-cloud-core-http:jar + com.google.cloud:google-cloud-monitoring:jar + com.google.http-client:google-http-client-gson:jar + org.apache.httpcomponents:httpclient:jar + org.apache.httpcomponents:httpcore:jar + com.google.j2objc:j2objc-annotations:jar + com.fasterxml.jackson.core::jar + com.fasterxml.jackson.dataformat::jar + com.fasterxml.jackson.datatype::jar + com.fasterxml.jackson::jar + com.github.stephenc.jcip:jcip-annotations:jar + net.java.dev.jna::jar + com.google.code.findbugs:jsr305:jar + com.squareup.okhttp3:okhttp:jar + com.squareup.okio::jar + io.opentelemetry::jar + com.google.cloud.opentelemetry:shared-resourcemapping:jar + org.codehaus.woodstox:stax2-api:jar + org.threeten::jar + org.lz4:lz4-java:jar + io.opentelemetry.contrib::jar + io.opentelemetry.semconv::jar + io.perfmark:perfmark-api:jar + com.google.re2j:re2j:jar + org.reactivestreams:reactive-streams:jar + io.projectreactor:reactor-core:jar + org.bouncycastle::jar + com.microsoft.azure:msal4j:jar + com.microsoft.azure:msal4j-persistence-extension:jar + com.amazonaws:aws-java-sdk-core:jar + com.amazonaws:aws-java-sdk-kms:jar + com.amazonaws:aws-java-sdk-s3:jar + com.nimbusds:oauth2-oidc-sdk:jar + com.azure:azure-core:jar + com.azure:azure-identity:jar + com.azure:azure-core-http-netty:jar + com.azure:azure-json:jar + com.microsoft.azure:azure-keyvault-core:jar + com.azure:azure-security-keyvault-secrets:jar + com.azure:azure-storage-blob:jar + com.azure:azure-storage-common:jar + com.azure:azure-storage-internal-avro:jar + com.azure:azure-xml:jar + com.nimbusds:nimbus-jose-jwt:jar + io.confluent::jar + com.google.code.gson:gson:jar + org.apache.commons:commons-collections4:jar + org.jspecify:jspecify:jar + org.apache.commons:commons-lang3:jar + commons-collections:commons-collections:jar + com.fasterxml.woodstox:woodstox-core:jar + com.ibm.icu:icu4j:jar + com.fasterxml.jackson.core:jackson-annotations:jar + software.amazon.awssdk:*:jar + software.amazon.eventstream:eventstream:jar + javax.annotation:javax.annotation-api:jar + javax.inject:javax.inject:jar + org.jctools:jctools-core:jar + com.nimbusds:lang-tag:jar + io.dropwizard.metrics:*:jar + com.eclipsesource.minimal-json:minimal-json:jar + org.slf4j:slf4j-api:jar + + org.apache.spark:spark-core_2.12:jar + org.apache.spark:spark-streaming_2.12:jar + org.apache.spark:spark-launcher_2.12:jar + org.apache.spark:spark-kvstore_2.12:jar + org.apache.spark:spark-network-common_2.12:jar + org.apache.spark:spark-network-shuffle_2.12:jar + org.apache.spark:spark-unsafe_2.12:jar + org.apache.spark:spark-tags_2.12:jar + + plugins/engines/beam/lib-beam + + + diff --git a/plugins/engines/beam/src/assembly/assembly-libs.xml b/plugins/engines/beam/src/assembly/assembly-libs.xml new file mode 100644 index 00000000000..4736424a1ca --- /dev/null +++ b/plugins/engines/beam/src/assembly/assembly-libs.xml @@ -0,0 +1,161 @@ + + + + + libs + + zip + + . + + + + + + + runtime + + org.apache.spark:spark-core_2.12:jar + org.apache.spark:spark-streaming_2.12:jar + org.apache.spark:spark-launcher_2.12:jar + org.apache.spark:spark-kvstore_2.12:jar + org.apache.spark:spark-network-common_2.12:jar + org.apache.spark:spark-network-shuffle_2.12:jar + org.apache.spark:spark-unsafe_2.12:jar + org.apache.spark:spark-tags_2.12:jar + + lib/spark-client + + + runtime + + org.apache.hadoop:*:jar + org.apache.hadoop.thirdparty:*:jar + org.yaml:snakeyaml:jar + org.jetbrains:annotations:jar + com.google.auto.value:auto-value-annotations:jar + org.apache.beam:beam-vendor-guava-32_1_2-jre:jar + com.github.luben:zstd-jni:jar + com.google.auth:google-auth-library-oauth2-http:jar + com.google.oauth-client:google-oauth-client:jar + com.google.api-client:google-api-client:jar + com.google.http-client:google-http-client:jar + com.google.http-client:google-http-client-apache-v2:jar + com.google.http-client:google-http-client-appengine:jar + google-http-client-gson:jar + com.google.http-client:google-http-client-jackson2:jar + com.google.android:annotations:jar + org.codehaus.mojo:animal-sniffer-annotations:jar + com.google.api:api-common:jar + com.google.auto.value:auto-value-annotations:jar + org.jetbrains.kotlin::jar + io.netty::jar + io.projectreactor.netty::jar + io.projectreactor:reactor-core:jar + io.grpc:grpc-netty-shaded:jar + org.apache.kafka::jar + com.google.cloud:google-cloud-storage:jar + io.grpc::jar + com.google.cloud:google-cloud-core-grpc:jar + com.google.api.grpc:grpc-google-cloud-storage-v2:jar + com.google.api:gax-grpc:jar + com.google.api:gax:jar + com.google.api.grpc::jar + org.checkerframework:checker-qual:jar + io.confluent:common-utils:jar + commons-cli:commons-cli:jar + commons-codec:commons-codec:jar + commons-logging:commons-logging:jar + org.apache.commons:commons-text:jar + com.google.cloud.opentelemetry:detector-resources-support:jar + com.google.errorprone:error_prone_annotations:jar + com.google.cloud.opentelemetry:exporter-metrics:jar + com.google.api:gax-httpjson:jar + com.google.apis:google-api-services-storage:jar + com.google.auth:google-auth-library-credentials:jar + com.google.auth:google-auth-library-oauth2-http:jar + com.google.cloud:google-cloud-core:jar + com.google.cloud:google-cloud-core-http:jar + com.google.cloud:google-cloud-monitoring:jar + com.google.http-client:google-http-client-gson:jar + org.apache.httpcomponents:httpclient:jar + org.apache.httpcomponents:httpcore:jar + com.google.j2objc:j2objc-annotations:jar + com.fasterxml.jackson.core::jar + com.fasterxml.jackson.dataformat::jar + com.fasterxml.jackson.datatype::jar + com.fasterxml.jackson::jar + com.github.stephenc.jcip:jcip-annotations:jar + net.java.dev.jna::jar + com.google.code.findbugs:jsr305:jar + com.squareup.okhttp3:okhttp:jar + com.squareup.okio::jar + io.opentelemetry::jar + io.opentelemetry.contrib::jar + io.opentelemetry.semconv::jar + com.google.cloud.opentelemetry:shared-resourcemapping:jar + org.codehaus.woodstox:stax2-api:jar + org.threeten::jar + org.lz4:lz4-java:jar + io.perfmark:perfmark-api:jar + com.google.re2j:re2j:jar + org.reactivestreams:reactive-streams:jar + org.bouncycastle::jar + com.microsoft.azure:msal4j:jar + com.microsoft.azure:msal4j-persistence-extension:jar + com.amazonaws:aws-java-sdk-core:jar + com.amazonaws:aws-java-sdk-kms:jar + com.amazonaws:aws-java-sdk-s3:jar + com.nimbusds:oauth2-oidc-sdk:jar + com.azure:azure-core:jar + com.azure:azure-identity:jar + com.azure:azure-core-http-netty:jar + com.azure:azure-json:jar + com.microsoft.azure:azure-keyvault-core:jar + com.azure:azure-security-keyvault-secrets:jar + com.azure:azure-storage-blob:jar + com.azure:azure-storage-common:jar + com.azure:azure-storage-internal-avro:jar + com.azure:azure-xml:jar + com.nimbusds:nimbus-jose-jwt:jar + io.confluent::jar + com.google.code.gson:gson:jar + org.apache.commons:commons-collections4:jar + org.apache.commons:commons-lang3:jar + commons-collections:commons-collections:jar + com.fasterxml.woodstox:woodstox-core:jar + com.ibm.icu:icu4j:jar + com.fasterxml.jackson.core:jackson-annotations:jar + software.amazon.awssdk:*:jar + software.amazon.eventstream:eventstream:jar + javax.annotation:javax.annotation-api:jar + javax.inject:javax.inject:jar + org.jctools:jctools-core:jar + com.nimbusds:lang-tag:jar + io.dropwizard.metrics:*:jar + com.eclipsesource.minimal-json:minimal-json:jar + org.slf4j:slf4j-api:jar + + lib/core + + + \ No newline at end of file diff --git a/plugins/engines/beam/src/assembly/assembly.xml b/plugins/engines/beam/src/assembly/assembly.xml index c012216d5cb..fe175665fb7 100644 --- a/plugins/engines/beam/src/assembly/assembly.xml +++ b/plugins/engines/beam/src/assembly/assembly.xml @@ -18,7 +18,8 @@ - hop-engines-beam + + plugin zip @@ -50,265 +51,9 @@ plugins/engines/beam - - runtime - - org.apache.hop:hop-engines-beam:jar - org.apache.hadoop:*:jar - org.apache.hadoop.thirdparty:*:jar - org.yaml:snakeyaml:jar - com.google.protobuf:protobuf-java:jar - com.google.protobuf:protobuf-java-util:jar - org.jetbrains:annotations:jar - com.google.auto.value:auto-value-annotations:jar - org.apache.beam:beam-vendor-guava-32_1_2-jre:jar - com.github.luben:zstd-jni:jar - com.google.auth:google-auth-library-oauth2-http:jar - com.google.oauth-client:google-oauth-client:jar - com.google.api-client:google-api-client:jar - com.google.http-client:google-http-client:jar - com.google.http-client:google-http-client-apache-v2:jar - com.google.http-client:google-http-client-appengine:jar - google-http-client-gson:jar - com.google.http-client:google-http-client-jackson2:jar - com.google.android:annotations:jar - org.codehaus.mojo:animal-sniffer-annotations:jar - com.google.api:api-common:jar - com.google.auto.value:auto-value-annotations:jar - com.google.guava::jar - org.jetbrains.kotlin::jar - io.netty::jar - org.codehaus.mojo:animal-sniffer-annotations:jar - io.projectreactor.netty::jar - io.grpc:grpc-netty-shaded:jar - io.grpc:grpc-xds:jar - org.apache.kafka::jar - org.xerial.snappy:snappy-java:jar - org.eclipse.jetty::jar - org.eclipse.jetty.compression::jar - com.google.cloud:google-cloud-storage:jar - io.grpc::jar - com.google.cloud:google-cloud-core-grpc:jar - com.google.api.grpc:grpc-google-cloud-storage-v2:jar - com.google.api:gax-grpc:jar - com.google.api.grpc::jar - org.checkerframework:checker-qual:jar - io.confluent:common-utils:jar - commons-cli:commons-cli:jar - commons-codec:commons-codec:jar - commons-logging:commons-logging:jar - org.apache.commons:commons-text:jar - com.google.cloud.opentelemetry:detector-resources-support:jar - com.google.errorprone:error_prone_annotations:jar - com.google.cloud.opentelemetry:exporter-metrics:jar - com.google.api:gax:jar - com.google.api:gax-httpjson:jar - com.google.apis:google-api-services-storage:jar - com.google.auth:google-auth-library-credentials:jar - com.google.auth:google-auth-library-oauth2-http:jar - com.google.cloud:google-cloud-core:jar - com.google.cloud:google-cloud-core-http:jar - com.google.cloud:google-cloud-monitoring:jar - com.google.http-client:google-http-client-gson:jar - org.apache.httpcomponents:httpclient:jar - org.apache.httpcomponents:httpcore:jar - com.google.j2objc:j2objc-annotations:jar - com.fasterxml.jackson.core::jar - com.fasterxml.jackson.dataformat::jar - com.fasterxml.jackson.datatype::jar - com.fasterxml.jackson::jar - com.github.stephenc.jcip:jcip-annotations:jar - net.java.dev.jna::jar - com.google.code.findbugs:jsr305:jar - com.squareup.okhttp3:okhttp:jar - com.squareup.okio::jar - io.opentelemetry::jar - com.google.cloud.opentelemetry:shared-resourcemapping:jar - org.codehaus.woodstox:stax2-api:jar - org.threeten::jar - org.lz4:lz4-java:jar - io.opentelemetry.contrib::jar - io.opentelemetry.semconv::jar - io.perfmark:perfmark-api:jar - com.google.re2j:re2j:jar - org.reactivestreams:reactive-streams:jar - io.projectreactor:reactor-core:jar - org.bouncycastle::jar - com.microsoft.azure:msal4j:jar - com.microsoft.azure:msal4j-persistence-extension:jar - com.amazonaws:aws-java-sdk-core:jar - com.amazonaws:aws-java-sdk-kms:jar - com.amazonaws:aws-java-sdk-s3:jar - com.nimbusds:oauth2-oidc-sdk:jar - com.azure:azure-core:jar - com.azure:azure-identity:jar - com.azure:azure-core-http-netty:jar - com.azure:azure-json:jar - com.microsoft.azure:azure-keyvault-core:jar - com.azure:azure-security-keyvault-secrets:jar - com.azure:azure-storage-blob:jar - com.azure:azure-storage-common:jar - com.azure:azure-storage-internal-avro:jar - com.azure:azure-xml:jar - com.nimbusds:nimbus-jose-jwt:jar - io.confluent::jar - com.google.code.gson:gson:jar - org.apache.commons:commons-collections4:jar - org.jspecify:jspecify:jar - org.apache.commons:commons-lang3:jar - commons-collections:commons-collections:jar - com.fasterxml.woodstox:woodstox-core:jar - com.ibm.icu:icu4j:jar - com.fasterxml.jackson.core:jackson-annotations:jar - software.amazon.awssdk:*:jar - software.amazon.eventstream:eventstream:jar - javax.annotation:javax.annotation-api:jar - javax.inject:javax.inject:jar - org.jctools:jctools-core:jar - com.nimbusds:lang-tag:jar - io.dropwizard.metrics:*:jar - com.eclipsesource.minimal-json:minimal-json:jar - org.slf4j:slf4j-api:jar - - org.apache.spark:spark-core_2.12:jar - org.apache.spark:spark-streaming_2.12:jar - org.apache.spark:spark-launcher_2.12:jar - org.apache.spark:spark-kvstore_2.12:jar - org.apache.spark:spark-network-common_2.12:jar - org.apache.spark:spark-network-shuffle_2.12:jar - org.apache.spark:spark-unsafe_2.12:jar - org.apache.spark:spark-tags_2.12:jar - - lib/beam - - - - runtime - - org.apache.spark:spark-core_2.12:jar - org.apache.spark:spark-streaming_2.12:jar - org.apache.spark:spark-launcher_2.12:jar - org.apache.spark:spark-kvstore_2.12:jar - org.apache.spark:spark-network-common_2.12:jar - org.apache.spark:spark-network-shuffle_2.12:jar - org.apache.spark:spark-unsafe_2.12:jar - org.apache.spark:spark-tags_2.12:jar - - lib/spark-client - - - runtime - - org.apache.hadoop:*:jar - org.apache.hadoop.thirdparty:*:jar - org.yaml:snakeyaml:jar - org.jetbrains:annotations:jar - com.google.auto.value:auto-value-annotations:jar - org.apache.beam:beam-vendor-guava-32_1_2-jre:jar - com.github.luben:zstd-jni:jar - com.google.auth:google-auth-library-oauth2-http:jar - com.google.oauth-client:google-oauth-client:jar - com.google.api-client:google-api-client:jar - com.google.http-client:google-http-client:jar - com.google.http-client:google-http-client-apache-v2:jar - com.google.http-client:google-http-client-appengine:jar - google-http-client-gson:jar - com.google.http-client:google-http-client-jackson2:jar - com.google.android:annotations:jar - org.codehaus.mojo:animal-sniffer-annotations:jar - com.google.api:api-common:jar - com.google.auto.value:auto-value-annotations:jar - org.jetbrains.kotlin::jar - io.netty::jar - io.projectreactor.netty::jar - io.projectreactor:reactor-core:jar - io.grpc:grpc-netty-shaded:jar - org.apache.kafka::jar - com.google.cloud:google-cloud-storage:jar - io.grpc::jar - com.google.cloud:google-cloud-core-grpc:jar - com.google.api.grpc:grpc-google-cloud-storage-v2:jar - com.google.api:gax-grpc:jar - com.google.api:gax:jar - com.google.api.grpc::jar - org.checkerframework:checker-qual:jar - io.confluent:common-utils:jar - commons-cli:commons-cli:jar - commons-codec:commons-codec:jar - commons-logging:commons-logging:jar - org.apache.commons:commons-text:jar - com.google.cloud.opentelemetry:detector-resources-support:jar - com.google.errorprone:error_prone_annotations:jar - com.google.cloud.opentelemetry:exporter-metrics:jar - com.google.api:gax-httpjson:jar - com.google.apis:google-api-services-storage:jar - com.google.auth:google-auth-library-credentials:jar - com.google.auth:google-auth-library-oauth2-http:jar - com.google.cloud:google-cloud-core:jar - com.google.cloud:google-cloud-core-http:jar - com.google.cloud:google-cloud-monitoring:jar - com.google.http-client:google-http-client-gson:jar - org.apache.httpcomponents:httpclient:jar - org.apache.httpcomponents:httpcore:jar - com.google.j2objc:j2objc-annotations:jar - com.fasterxml.jackson.core::jar - com.fasterxml.jackson.dataformat::jar - com.fasterxml.jackson.datatype::jar - com.fasterxml.jackson::jar - com.github.stephenc.jcip:jcip-annotations:jar - net.java.dev.jna::jar - com.google.code.findbugs:jsr305:jar - com.squareup.okhttp3:okhttp:jar - com.squareup.okio::jar - io.opentelemetry::jar - io.opentelemetry.contrib::jar - io.opentelemetry.semconv::jar - com.google.cloud.opentelemetry:shared-resourcemapping:jar - org.codehaus.woodstox:stax2-api:jar - org.threeten::jar - org.lz4:lz4-java:jar - io.perfmark:perfmark-api:jar - com.google.re2j:re2j:jar - org.reactivestreams:reactive-streams:jar - org.bouncycastle::jar - com.microsoft.azure:msal4j:jar - com.microsoft.azure:msal4j-persistence-extension:jar - com.amazonaws:aws-java-sdk-core:jar - com.amazonaws:aws-java-sdk-kms:jar - com.amazonaws:aws-java-sdk-s3:jar - com.nimbusds:oauth2-oidc-sdk:jar - com.azure:azure-core:jar - com.azure:azure-identity:jar - com.azure:azure-core-http-netty:jar - com.azure:azure-json:jar - com.microsoft.azure:azure-keyvault-core:jar - com.azure:azure-security-keyvault-secrets:jar - com.azure:azure-storage-blob:jar - com.azure:azure-storage-common:jar - com.azure:azure-storage-internal-avro:jar - com.azure:azure-xml:jar - com.nimbusds:nimbus-jose-jwt:jar - io.confluent::jar - com.google.code.gson:gson:jar - org.apache.commons:commons-collections4:jar - org.apache.commons:commons-lang3:jar - commons-collections:commons-collections:jar - com.fasterxml.woodstox:woodstox-core:jar - com.ibm.icu:icu4j:jar - com.fasterxml.jackson.core:jackson-annotations:jar - software.amazon.awssdk:*:jar - software.amazon.eventstream:eventstream:jar - javax.annotation:javax.annotation-api:jar - javax.inject:javax.inject:jar - org.jctools:jctools-core:jar - com.nimbusds:lang-tag:jar - io.dropwizard.metrics:*:jar - com.eclipsesource.minimal-json:minimal-json:jar - org.slf4j:slf4j-api:jar - - lib/core - - \ No newline at end of file + + + ${project.basedir}/src/assembly/assembly-beam-runtime.xml + + diff --git a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java index c6119d456e6..6801d5eb756 100644 --- a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java +++ b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java @@ -459,8 +459,9 @@ public static File resolveSparkClientPackDir(String sparkClientVersion) { } /** - * @param versionedPack when true, also skip {@code spark-*.jar} under {@code lib/beam} so Beam's - * fixed Spark fragments cannot mix with an alternate client pack + * @param versionedPack when true, also skip {@code spark-*.jar} under Beam SDK folders ({@code + * lib/beam} legacy or {@code plugins/engines/beam/lib-beam}) so Beam's fixed Spark fragments + * cannot mix with an alternate client pack */ private static void collectJarsExcludingSparkClientPacks( File dir, Set jarFiles, boolean versionedPack) { @@ -483,11 +484,12 @@ private static void collectJarsExcludingSparkClientPacks( if (child.isDirectory()) { collectJarsExcludingSparkClientPacks(child, jarFiles, versionedPack); } else if (child.isFile() && child.getName().endsWith(".jar")) { - // Versioned packs own all spark-* jars; drop Beam's spark fragments from lib/beam + // Versioned packs own all spark-* jars; drop Beam's spark fragments from lib-beam / + // lib/beam if (versionedPack && child.getName().startsWith("spark-") && parent != null - && "beam".equals(parent.getName())) { + && isBeamSdkJarFolder(parent)) { continue; } jarFiles.add(child); @@ -495,6 +497,19 @@ private static void collectJarsExcludingSparkClientPacks( } } + /** True for legacy {@code lib/beam} or marketplace {@code …/beam/lib-beam}. */ + static boolean isBeamSdkJarFolder(File folder) { + if (folder == null) { + return false; + } + String name = folder.getName(); + if ("lib-beam".equals(name)) { + return true; + } + File parent = folder.getParentFile(); + return "beam".equals(name) && parent != null && "lib".equals(parent.getName()); + } + /** * Add a toolbar item just above the pipeline graph to quickly visit the execution of a Dataflow * pipeline in the GCP console. diff --git a/plugins/misc/marketplace/README.md b/plugins/misc/marketplace/README.md new file mode 100644 index 00000000000..101148c6feb --- /dev/null +++ b/plugins/misc/marketplace/README.md @@ -0,0 +1,494 @@ + + +# Hop Marketplace plugin + +Bundled core plugin that installs optional Hop plugins from Maven repositories. +**Default primary:** [Apache Repository](https://repository.apache.org/content/groups/public/) +(`repository.apache.org`), with **Maven Central** as fallback. + +## CLI + +From the Hop install directory (or invoke the `hop` script by path — it already +`cd`s to the install root): + +```bash +./hop marketplace install hop-tech-parquet +./hop marketplace install hop-engines-spark:2.19.0 --repo asf +./hop marketplace install hop-engines-beam +./hop marketplace install datavault # short name → discovered GAV + version +./hop marketplace list +./hop marketplace query parquet +./hop marketplace query engines +./hop marketplace uninstall hop-tech-parquet + +# Repositories (saved to ${HOP_CONFIG_FOLDER}/hop-config.json) +./hop marketplace repo list +./hop marketplace repo add --id local-nexus --url http://127.0.0.1:8081/repository/hop-plugins/ --primary +./hop marketplace repo add --id community --url https://nexus.example/repository/hop/ --browse +./hop marketplace repo import hop-marketplace-repo.yaml +./hop marketplace repo export community -o community-marketplace.yaml +./hop marketplace repo set-primary asf +./hop marketplace repo reset-defaults + +# Discovery: Apache optional catalog + every browse=true repo (live Nexus zip list) +# Default: ASCII table. --csv for scripts. --include-gav adds groupId:artifactId:version. +./hop marketplace query parquet +./hop marketplace query datavault +./hop marketplace query vault --repo community +./hop marketplace query datavault --include-gav +./hop marketplace query --csv > plugins.csv + +# Declarative environment (CI/CD / Docker) +./hop marketplace apply -f hop-env.yaml +./hop marketplace apply -f hop-env.yaml --prune +./hop marketplace validate -f hop-env.yaml + +# Restore all optional plugins that used to ship in the fat client +./hop marketplace apply -f full-client-env.yaml +``` + +Install tries the **primary** repository first, then other **enabled** repositories +in list order, until the zip is found (or all fail). Use `--repo ` to force a +single repository. + +Coordinates: + +- `artifactId` (uses config `defaultVersion` / Hop version) +- `artifactId:version` +- `groupId:artifactId:version` + +After install or uninstall, **restart Hop** so the plugin registry reloads. + +### hop-env.yaml + +See `config/projects/samples/marketplace/hop-env.example.yaml` (after install) or +`plugins/misc/marketplace/src/main/samples/hop-env.example.yaml` in source. + +```yaml +version: "1.0" +hopVersion: "2.19.0" +enforceOnRun: false # set true to hard-fail hop-run on drift +repositories: + - id: asf + url: "https://repository.apache.org/content/groups/public/" + - id: central + url: "https://repo1.maven.org/maven2/" +plugins: + - artifactId: hop-tech-parquet + version: "2.19.0" +dependencies: + - groupId: org.postgresql + artifactId: postgresql + version: "42.7.3" +``` + +`hop-run` checks the env file only when `enforceOnRun: true` or `-Dhop.env.enforce=true`. +Discovery order: `-f` / `HOP_ENV_FILE` / `PROJECT_HOME/hop-env.yaml` / install-root `hop-env.yaml`. + +### Projects lifecycle environments + +When the **Projects** plugin enables a lifecycle environment, Marketplace can +check the same hop-env file using settings stored as **namespaced attributes** +on that environment (`IAttributes` group `marketplace`). No compile-time +dependency exists between the two plugins; they share core `AttributesContext`. + +| Attribute key (`marketplace` group) | Values | Meaning | +|-------------------------------------|--------|---------| +| `envFile` | path (variables OK) | hop-env / full-client-env file | +| `onEnable` | `off` / `warn` / `enforce` | Action when the environment is enabled | +| `strict` | `true` / `false` | Extra marketplace plugins count as drift | +| `autoApply` | `true` / `false` | Apply missing plugins on enable (default off) | + +If `onEnable` is unset, purpose defaults apply: **Production** → `enforce`, +**Testing/Acceptance** → `warn`, **Development/CI/CB** → `off`. + +Configure these on the environment dialog **Marketplace plugins** tab (only +visible when this plugin is installed). At enable time extension point +`HopProjectEnvironmentAfterEnabled` runs the check. + +## Config (`hop-config.json`) + +Marketplace settings live under the `marketplace` key in **`${HOP_CONFIG_FOLDER}/hop-config.json`** +(always that path). If `HOP_CONFIG_FOLDER` is unset, Hop defaults it to +`/config` (with the hop launcher: `/config`). + +When the `marketplace` key is absent, defaults are: + +1. **asf** (primary) — `https://repository.apache.org/content/groups/public/` +2. **central** — `https://repo1.maven.org/maven2/` + +```json +{ + "marketplace": { + "enabled": true, + "groupId": "org.apache.hop", + "defaultVersion": "2.19.0-SNAPSHOT", + "repositories": [ + { + "id": "asf", + "name": "Apache Repository", + "url": "https://repository.apache.org/content/groups/public/", + "primary": true, + "enabled": true + }, + { + "id": "central", + "name": "Maven Central", + "url": "https://repo1.maven.org/maven2/", + "primary": false, + "enabled": true + } + ] + } +} +``` + +Local Sonatype Nexus for development (see `docker/marketplace-nexus/README.md`): + +```bash +./hop marketplace repo add --id local-nexus \ + --url http://127.0.0.1:8081/repository/hop-plugins/ --primary +./hop marketplace install hop-tech-parquet +``` + +Anonymous local Nexus needs **no** username/password and **no** `HOP_MARKETPLACE_*` +env (wrong Basic auth causes HTTP 401 even when anonymous would work). + +Corporate private repos can use Basic auth (prefer a read-only user): + +```bash +export HOP_MARKETPLACE_USERNAME=reader +export HOP_MARKETPLACE_PASSWORD='…' +``` + +## GUI + +Hop GUI → **Tools → Marketplace…**, or the main toolbar icon after **Save As…** +(separator before it), opens the marketplace. The startup **Welcome** dialog also has a +**Marketplace** topic (docs link, open dialog, install-all via `full-client-env.yaml`). +The **Plugins** tab lists Apache optionals plus a live zip list from every enabled +repository with **Browse** turned on (same as `marketplace query`). The **Repositories** +tab only manages endpoints (add/edit, Import/Export, primary, browse flag) — it does not +list plugins. Install uses the primary repository first, then fallbacks. + +## Company marketplace (shareable repo definition) + +Three steps: + +```bash +./hop marketplace repo import hop-marketplace-repo.yaml # browse: true +./hop marketplace query datavault # no special flags +./hop marketplace install datavault # resolves hop-datavault + version from repo +# or explicit: ./hop marketplace install org.apache.hop:hop-datavault:0.4.0-SNAPSHOT +``` + + +**How listing works** + +- Bundled Apache optional plugins: **version = running Hop version** (not absolute latest on ASF) +- Every enabled repo with **`browse: true`**: live Nexus search for `*.zip` (one row per artifact, latest plugin version, last updated) +- Optional YAML `plugins:` metadata **enriches** names/categories and can set: + - `minHopVersion` / `maxHopVersion` — hide if this Hop is outside the range (e.g. datavault needs ≥ 2.18.1) + - `version` — optional pin of the plugin artifact; otherwise latest from browse + +GUI **Plugins** tab uses the same discovery as `query`. + +Sample: `src/main/samples/hop-marketplace-repo.community.example.yaml` + +**Environment file** path + **Browse…** / **Edit…** / **Validate** / **Apply** manage +declarative `hop-env.yaml` files. **Edit…** opens a tabbed editor (General, +Repositories, Plugins, Dependencies) with New/Open/Save/Save As, catalog multi-add, +and optional import of repositories from hop-config. + +Restart after install, apply, or uninstall. + +## Optional plugin registry (source of truth) + +All marketplace-optional plugins are declared in **one file only**: + +```text +plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml +``` + +Do not hardcode plugin lists elsewhere. Everything below reads this registry: + +| Consumer | How it uses the registry | +|----------|--------------------------| +| GUI catalog / `hop marketplace query` | Loads YAML at runtime | +| `full-client-env.yaml` | Generated by `tools/generate-full-client-env.sh` (Maven `generate-resources`) | +| Local publish to Nexus/Artifactory | `docker/marketplace-nexus/publish-marketplace-plugins.sh` | +| Unpack zips into an install (IT images) | `tools/install-wave1-plugins.sh` via `tools/list-marketplace-plugins.sh` | + +Each entry needs at least: + +```yaml + - artifactId: hop-tech-parquet + name: Parquet + category: Technology + description: Parquet file format support. + installPath: plugins/tech/parquet # path under Hop home after install + modulePath: plugins/tech/parquet # Maven module in this repo (for package/publish) +``` + +Top-level `groupId` (default `org.apache.hop`) is used when publishing. + +### Making a plugin optional (checklist) + +1. Exclude it from the fat hop-client assembly. +2. **Add one entry** to `optional-plugins.yaml` (`artifactId`, `installPath`, `modulePath`, …). +3. Rebuild marketplace / client so `full-client-env.yaml` regenerates. +4. For local testing: package + publish the zip (see next section), point Hop at that repo, + then `./hop marketplace apply -f full-client-env.yaml` (or install a single GAV). +5. Restart Hop. + +## Publishing plugin zips (local Nexus / Artifactory) + +Marketplace installs **Maven GAV zips**, not jars alone. Layout: + +```text +{repoBase}/org/apache/hop/{artifactId}/{version}/{artifactId}-{version}.zip +``` + +Plugin modules already produce that zip via the reactor assembly +(`appendAssemblyId=false` → `modulePath/target/{artifactId}-{version}.zip`). + +### Script: `publish-marketplace-plugins.sh` + +From the **Hop repo root**: + +```bash +# List what the registry will publish (no network) +./tools/list-marketplace-plugins.sh +./tools/list-marketplace-plugins.sh --zips +``` + +#### Local Sonatype Nexus (Docker) + +```bash +./docker/marketplace-nexus/start.sh +# UI http://localhost:8081/ — admin / hop-nexus-dev +# Repo http://127.0.0.1:8081/repository/hop-plugins/ + +export NEXUS_PASSWORD=hop-nexus-dev + +# Build every registry modulePath, then deploy all zips: +./docker/marketplace-nexus/publish-marketplace-plugins.sh --package + +# Or deploy only zips that already exist under target/: +./docker/marketplace-nexus/publish-marketplace-plugins.sh + +# Preview without uploading: +./docker/marketplace-nexus/publish-marketplace-plugins.sh --dry-run +``` + +Prefer **`127.0.0.1`** over `localhost` in Hop config (IPv6 vs Docker IPv4). + +More detail: `docker/marketplace-nexus/README.md`. + +#### Your Artifactory (or any Maven 2 repo) + +```bash +export ARTIFACTORY_URL='https://artifactory.example.com/artifactory/hop-plugins-local' +export ARTIFACTORY_USER='deploy-user' +export ARTIFACTORY_PASSWORD='…' +export NEXUS_REPO_ID=artifactory # must match used for deploy auth +export HOP_VERSION=2.19.0-SNAPSHOT # optional; default 2.19.0-SNAPSHOT + +./docker/marketplace-nexus/publish-marketplace-plugins.sh --package +``` + +Equivalent env names also accepted: `NEXUS_REPO_URL`, `NEXUS_USER`, `NEXUS_PASSWORD`, +`NEXUS_ADMIN_PASSWORD`. + +| Flag / env | Meaning | +|------------|---------| +| `--package` | `mvn -pl -am package -DskipTests`, then deploy | +| `--dry-run` | Print deploy targets; no Maven deploy (and no password required) | +| `HOP_VERSION` | Version coordinate and zip filename (default `2.19.0-SNAPSHOT`) | +| `GROUP_ID` | Override group (default: `groupId` from the YAML, else `org.apache.hop`) | +| `NEXUS_REPO_ID` | Maven `settings.xml` server id for credentials (default `hop-plugins`) | + +Missing zips are **skipped** with a message (`SKIP (not built)`), not a hard failure—so you +can publish a subset after a partial package. + +`publish-wave1-plugins.sh` is a **deprecated alias** of `publish-marketplace-plugins.sh`. + +### Point Hop at the local repo and install + +```bash +cd /path/to/hop-client # install root (hop launcher cd's here) + +./hop marketplace repo add --id local-nexus \ + --url http://127.0.0.1:8081/repository/hop-plugins/ --primary + +./hop marketplace install hop-tech-parquet +# or restore everything listed in the registry / full-client env: +./hop marketplace apply -f full-client-env.yaml +``` + +Anonymous **read** on the repo is ideal for installs. Deploy credentials stay only on the +publish side (admin / CI user). + +### Related helper scripts + +```bash +# artifactId|modulePath (or --zips / --modules) +./tools/list-marketplace-plugins.sh + +# Unzip all built registry zips into a Hop install (no Maven repo) +./tools/install-wave1-plugins.sh [HOP_INSTALL_DIR] + +# Regenerate full-client-env.yaml (also run by marketplace module generate-resources) +./tools/generate-full-client-env.sh [HOP_VERSION] [OUTPUT_PATH] +``` + +## Packaging & size + +Optional plugins are **not** bundled in the default hop-client zip so the download stays +under the ASF packaging limit. + +### Production / ASF Nexus + +Release `release:perform` uses `-P=-assemblies -DskipTests` (root `pom.xml`): + +- Skips the `assemblies/` reactor (hop-client goes to dist.apache.org). +- Keeps the per-module **`assembly`** profile so each plugin zip is attached and + staged to `repository.apache.org`, then Maven Central after the vote. + +Example after release: + +```text +https://repository.apache.org/content/groups/public/org/apache/hop/hop-tech-parquet//hop-tech-parquet-.zip +``` + +### SNAPSHOT (CI) + +Jenkins on **main** does two steps (see `Jenkinsfile`): + +1. `mvn … -DaltDeploymentRepository=…::file:./local-snapshots-dir clean deploy` + (full reactor: tests, assemblies, plugin zips) +2. `mvn -P deploy-snapshots wagon:upload` → ASF snapshots + (`repository.apache.org`). Plugin zips are **included**; hop-assemblies and + core/engine/ui zips stay excluded. + +**Pre-merge confidence (does not need ASF credentials):** + +```bash +# 1) Same deploy *layout* as Jenkins step 1 (skipTests optional on a laptop) +rm -rf local-snapshots-dir && mkdir local-snapshots-dir +./mvnw -T 2 -B -DskipTests \ + -DaltDeploymentRepository=snapshot-repo::default::file:$(pwd)/local-snapshots-dir \ + clean deploy + +# 2) Every optional-plugins.yaml artifact must have a .zip in that tree +./tools/verify-ci-snapshot-zips.sh + +# Full CI flags (longer; needs xvfb for UI tests) — closer to Jenkins: +# xvfb-run -a --server-args='-screen 0 1280x1024x24' ./mvnw -T 2 -U -B -e -fae -V \ +# -Dmaven.compiler.fork=true -Dsurefire.rerunFailingTestsCount=2 -DSkipTestContainers=true \ +# -DaltDeploymentRepository=snapshot-repo::default::file:$(pwd)/local-snapshots-dir \ +# clean deploy +``` + +**After merge to apache/hop main:** wait for Jenkins Deploy green, then check e.g. + +`https://repository.apache.org/content/repositories/snapshots/org/apache/hop/hop-tech-parquet/` + +for a `.zip` under the SNAPSHOT folder. Private data-hopper deploys do **not** replace this check. + +Point marketplace at ASF snapshots when testing SNAPSHOT Hop builds from Apache CI. + +### SNAPSHOT to a private Nexus (e.g. data-hopper) + +For local or private builders (same idea as Jenkins deploy, different target), use +a Maven server id + URL such as: + +```text +https://repository.data-hopper.com/repository/apache-hop-plugins/ +``` + +**Do not** put deploy passwords in the Hop git tree. Use env vars or `~/.m2/settings.xml`. + +**Fast path — optional plugin zips only** (recommended for marketplace work): + +```bash +export HOP_DEPLOY_USER=hop_build +export HOP_DEPLOY_PASSWORD='…' # from your password manager +export DATA_HOPPER_URL='https://repository.data-hopper.com/repository/apache-hop-plugins' +./tools/deploy-snapshots-data-hopper.sh marketplace +``` + +**Jenkins-like full reactor** (jars + plugin zips; skips `assemblies/` hop-client module): + +```xml + + + apache-hop-plugins + hop_build + + +``` + +```bash +./tools/deploy-snapshots-data-hopper.sh reactor +# equivalent: +# ./mvnw clean deploy -DskipTests -P=-assemblies \ +# -DaltDeploymentRepository=apache-hop-plugins::default::https://repository.data-hopper.com/repository/apache-hop-plugins/ +``` + +Then point Hop at the repo: + +```bash +./hop marketplace repo add --id data-hopper \ + --url https://repository.data-hopper.com/repository/apache-hop-plugins/ --primary +./hop marketplace install hop-tech-parquet +``` + +Verify zips: + +```bash +export NEXUS_REPO_URL='https://repository.data-hopper.com/repository/apache-hop-plugins/' +./docker/marketplace-nexus/dummy-staging.sh --skip-publish +``` + +Official product defaults remain ASF public + Maven Central; private Nexus is optional. + +### Local Nexus / Artifactory + +Offline / sandbox work still uses `docker/marketplace-nexus/` and +`publish-marketplace-plugins.sh` as documented above. + +**Dummy staging** (publish every registry zip, HTTP-verify Maven layout, CLI install smoke): + +```bash +./docker/marketplace-nexus/start.sh +export NEXUS_PASSWORD=hop-nexus-dev +./docker/marketplace-nexus/dummy-staging.sh +``` + +Same script can validate an ASF staging repo with +`NEXUS_REPO_URL= ./docker/marketplace-nexus/dummy-staging.sh --skip-publish`. + +Lean client size check: + +```bash +./tools/check-assembly-size.sh +# typically hop-client well under the 850 MB limit +``` diff --git a/plugins/misc/marketplace/pom.xml b/plugins/misc/marketplace/pom.xml new file mode 100644 index 00000000000..f9aaf689aab --- /dev/null +++ b/plugins/misc/marketplace/pom.xml @@ -0,0 +1,79 @@ + + + + 4.0.0 + + + org.apache.hop + hop-plugins-misc + 2.19.0-SNAPSHOT + + + hop-misc-marketplace + jar + Hop Plugins Miscellaneous Marketplace + + + + + org.apache.hop + hop-libs + ${project.version} + pom + import + + + + + + + + org.yaml + snakeyaml + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + + + generate-full-client-env + + exec + + generate-resources + + bash + + ${maven.multiModuleProjectDirectory}/tools/generate-full-client-env.sh + ${project.version} + ${project.build.directory}/generated/full-client-env.yaml + + + + + + + + diff --git a/plugins/misc/marketplace/src/assembly/assembly.xml b/plugins/misc/marketplace/src/assembly/assembly.xml new file mode 100644 index 00000000000..9937d0f2546 --- /dev/null +++ b/plugins/misc/marketplace/src/assembly/assembly.xml @@ -0,0 +1,57 @@ + + + + + hop-misc-marketplace + + zip + + . + + + ${project.basedir}/src/main/resources/version.xml + ${hop.plugin.libdir} + true + + + + ${project.build.directory}/generated/full-client-env.yaml + . + full-client-env.yaml + + + ${project.build.directory}/generated/full-client-env.yaml + config/projects/samples/marketplace/ + full-client-env.yaml + + + + + + ${project.basedir}/src/main/samples + config/projects/samples/marketplace/ + + + + + ${maven.multiModuleProjectDirectory}/assemblies/shared/hop-plugin-libs.xml + + diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/NexusRepositoryBrowser.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/NexusRepositoryBrowser.java new file mode 100644 index 00000000000..de49c971e31 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/NexusRepositoryBrowser.java @@ -0,0 +1,321 @@ +/* + * 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.marketplace.catalog; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.resolve.SnapshotVersions; + +/** + * Best-effort browse of a Sonatype Nexus 3 hosted/proxy Maven repository via REST search for {@code + * maven.extension=zip} assets. + */ +public final class NexusRepositoryBrowser { + + private NexusRepositoryBrowser() {} + + public static List browse( + MarketplaceRepository repository, String textFilter, ILogChannel log) throws HopException { + if (repository == null || StringUtils.isBlank(repository.getUrl())) { + return List.of(); + } + String repoName = extractRepositoryName(repository.getUrl()); + String nexusBase = extractNexusBase(repository.getUrl()); + if (StringUtils.isBlank(repoName) || StringUtils.isBlank(nexusBase)) { + throw new HopException( + "Cannot derive Nexus host/repository name from URL: " + repository.getUrl()); + } + + HttpClient client = + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + + // One row per groupId:artifactId — keep the latest plugin version (not every unique SNAPSHOT). + Map byGa = new LinkedHashMap<>(); + String continuation = null; + int pages = 0; + do { + pages++; + if (pages > 50) { + if (log != null) { + log.logBasic("Nexus search pagination stopped after 50 pages for " + repoName); + } + break; + } + StringBuilder q = new StringBuilder(); + q.append(nexusBase) + .append("/service/rest/v1/search?repository=") + .append(enc(repoName)) + .append("&format=maven2&maven.extension=zip"); + if (StringUtils.isNotBlank(repository.getGroupIdFilter())) { + q.append("&group=").append(enc(repository.getGroupIdFilter())); + } + if (StringUtils.isNotBlank(textFilter)) { + q.append("&name=").append(enc(textFilter.trim())); + } else if (StringUtils.isNotBlank(repository.getSearchQuery())) { + q.append("&name=").append(enc(repository.getSearchQuery().trim())); + } + if (StringUtils.isNotBlank(continuation)) { + q.append("&continuationToken=").append(enc(continuation)); + } + + String body = getText(client, q.toString(), repository); + Map json; + try { + json = HopJson.newMapper().readValue(body, Map.class); + } catch (Exception e) { + throw new HopException("Unable to parse Nexus search JSON from " + q, e); + } + Object items = json.get("items"); + if (items instanceof List list) { + for (Object item : list) { + if (!(item instanceof Map m)) { + continue; + } + String group = str(m.get("group")); + String name = str(m.get("name")); + String rawVersion = str(m.get("version")); + if (StringUtils.isAnyBlank(group, name, rawVersion)) { + continue; + } + // Nexus reports unique SNAPSHOT timestamps (1.0-20260721.105615-1); Maven layout uses + // base folder 1.0-SNAPSHOT/. Prefer the version folder from the zip asset path. + String zipAssetPath = extractZipAssetPath(m); + String version = SnapshotVersions.toBaseVersion(rawVersion, zipAssetPath); + if (!repository.isIncludeSnapshots() && SnapshotVersions.isSnapshot(version)) { + continue; + } + String gaKey = group.toLowerCase(Locale.ROOT) + ":" + name.toLowerCase(Locale.ROOT); + String lastUpdated = extractLastUpdated(m); + OptionalPluginInfo existing = byGa.get(gaKey); + if (existing != null && !isPreferable(version, lastUpdated, existing)) { + continue; + } + String path = + StringUtils.isNotBlank(zipAssetPath) + ? zipAssetPath.replaceFirst("^/+", "") + : mavenPath(group, name, version); + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setGroupId(group); + info.setArtifactId(name); + info.setVersion(version); + info.setName(name); + info.setCategory("auto-discovered"); + info.setDescription(path); + info.setInstallPath(path); + info.setLastUpdated(lastUpdated); + info.setSource(repository.getId()); + byGa.put(gaKey, info); + } + } + Object token = json.get("continuationToken"); + continuation = + token == null || StringUtils.isBlank(String.valueOf(token)) + ? null + : String.valueOf(token); + } while (continuation != null); + + List out = new ArrayList<>(byGa.values()); + if (StringUtils.isNotBlank(textFilter) || StringUtils.isNotBlank(repository.getSearchQuery())) { + // client-side refine (server name= is only a partial match) + String needle = StringUtils.isNotBlank(textFilter) ? textFilter : repository.getSearchQuery(); + out = RemotePluginCatalog.filter(out, repository, needle); + } + return out; + } + + /** + * Prefer higher plugin version; if versions equal, prefer newer lastUpdated (unique SNAPSHOT + * builds collapsed to the same base version). + */ + static boolean isPreferable( + String candidateVersion, String candidateUpdated, OptionalPluginInfo existing) { + if (existing == null) { + return true; + } + int vc = VersionCompat.compare(candidateVersion, existing.getVersion()); + if (vc > 0) { + return true; + } + if (vc < 0) { + return false; + } + return isNewer(candidateUpdated, existing.getLastUpdated()); + } + + static String extractRepositoryName(String repositoryUrl) { + if (StringUtils.isBlank(repositoryUrl)) { + return null; + } + String u = repositoryUrl.trim(); + while (u.endsWith("/")) { + u = u.substring(0, u.length() - 1); + } + int idx = u.indexOf("/repository/"); + if (idx < 0) { + return null; + } + String rest = u.substring(idx + "/repository/".length()); + int slash = rest.indexOf('/'); + return slash < 0 ? rest : rest.substring(0, slash); + } + + static String extractNexusBase(String repositoryUrl) { + if (StringUtils.isBlank(repositoryUrl)) { + return null; + } + String u = repositoryUrl.trim(); + int idx = u.indexOf("/repository/"); + if (idx < 0) { + // already a host root? + while (u.endsWith("/")) { + u = u.substring(0, u.length() - 1); + } + return u; + } + return u.substring(0, idx); + } + + private static String getText(HttpClient client, String url, MarketplaceRepository repository) + throws HopException { + try { + HttpRequest.Builder b = + HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(60)).GET(); + if (repository.hasCredentials()) { + String token = + Base64.getEncoder() + .encodeToString( + (repository.effectiveUsername() + ":" + repository.effectivePassword()) + .getBytes(StandardCharsets.UTF_8)); + b.header("Authorization", "Basic " + token); + } + HttpResponse response = client.send(b.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new HopException("Nexus search failed HTTP " + response.statusCode() + " for " + url); + } + return response.body(); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Nexus search failed for " + url, e); + } + } + + private static String enc(String s) { + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + private static String str(Object o) { + if (o == null) { + return null; + } + String s = String.valueOf(o).trim(); + return s.isEmpty() ? null : s; + } + + static String mavenPath(String group, String artifact, String version) { + return group.replace('.', '/') + "/" + artifact + "/" + version + "/"; + } + + /** + * Path of the first {@code .zip} asset (not checksum sidecars), relative to the repository root. + */ + static String extractZipAssetPath(Map item) { + Object assets = item.get("assets"); + if (!(assets instanceof List list)) { + return null; + } + for (Object o : list) { + if (!(o instanceof Map asset)) { + continue; + } + String path = str(asset.get("path")); + if (path == null) { + continue; + } + String lower = path.toLowerCase(Locale.ROOT); + if (lower.endsWith(".zip") + && !lower.endsWith(".zip.sha1") + && !lower.endsWith(".zip.md5") + && !lower.endsWith(".zip.sha256") + && !lower.endsWith(".zip.sha512")) { + return path; + } + } + return null; + } + + static String extractLastUpdated(Map item) { + Object assets = item.get("assets"); + if (assets instanceof List list && !list.isEmpty()) { + // Prefer zip asset lastModified when present + for (Object o : list) { + if (o instanceof Map asset) { + String path = str(asset.get("path")); + if (path != null && path.toLowerCase(Locale.ROOT).endsWith(".zip")) { + String lm = str(asset.get("lastModified")); + if (lm != null) { + return lm; + } + } + } + } + Object first = list.get(0); + if (first instanceof Map asset) { + String lm = str(asset.get("lastModified")); + if (lm != null) { + return lm; + } + } + } + String assetsUpdated = str(item.get("assetsUpdated")); + if (assetsUpdated != null) { + return assetsUpdated; + } + return str(item.get("lastModified")); + } + + /** Prefer non-blank newer lastUpdated strings (ISO-8601 sorts lexicographically). */ + static boolean isNewer(String candidate, String existing) { + if (StringUtils.isBlank(existing)) { + return true; + } + if (StringUtils.isBlank(candidate)) { + return false; + } + return candidate.compareTo(existing) > 0; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalog.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalog.java new file mode 100644 index 00000000000..e62767e0d45 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalog.java @@ -0,0 +1,237 @@ +/* + * 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.marketplace.catalog; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.yaml.snakeyaml.Yaml; + +/** + * Catalog of marketplace-optional plugins loaded from {@code optional-plugins.yaml} on the + * classpath (source of truth for Wave 1+ optional plugins and full-client-env generation). + */ +public final class OptionalPluginCatalog { + + public static final String REGISTRY_RESOURCE = + "/org/apache/hop/marketplace/optional-plugins.yaml"; + + private static final List PLUGINS = loadRegistry(); + + private OptionalPluginCatalog() {} + + public static List listWave1() { + return listOptional(); + } + + /** All marketplace-optional plugins from the registry. */ + public static List listOptional() { + return PLUGINS; + } + + /** + * Case-insensitive substring filter over artifactId, name, category, description, and + * installPath. Blank filter returns all optional plugins. + */ + public static List query(String filter) { + if (StringUtils.isBlank(filter)) { + return listOptional(); + } + String needle = filter.trim().toLowerCase(); + List matches = new ArrayList<>(); + for (OptionalPluginInfo info : PLUGINS) { + if (matchesFilter(info, needle)) { + matches.add(info); + } + } + return Collections.unmodifiableList(matches); + } + + private static boolean matchesFilter(OptionalPluginInfo info, String needleLower) { + return containsIgnoreCase(info.getArtifactId(), needleLower) + || containsIgnoreCase(info.getName(), needleLower) + || containsIgnoreCase(info.getCategory(), needleLower) + || containsIgnoreCase(info.getDescription(), needleLower) + || containsIgnoreCase(info.getInstallPath(), needleLower); + } + + private static boolean containsIgnoreCase(String value, String needleLower) { + return value != null && value.toLowerCase().contains(needleLower); + } + + public static boolean isInstalledOnDisk(Path hopHome, OptionalPluginInfo info) { + if (info.getInstallPath() == null) { + return false; + } + Path path = hopHome.resolve(info.getInstallPath()); + return Files.isDirectory(path); + } + + private static List loadRegistry() { + try (InputStream in = OptionalPluginCatalog.class.getResourceAsStream(REGISTRY_RESOURCE)) { + if (in == null) { + return Collections.unmodifiableList(fallbackCatalog()); + } + Yaml yaml = new Yaml(); + Object loaded = yaml.load(in); + if (!(loaded instanceof Map)) { + return Collections.unmodifiableList(fallbackCatalog()); + } + Map root = (Map) loaded; + Object pluginsObj = root.get("plugins"); + if (!(pluginsObj instanceof List)) { + return Collections.unmodifiableList(fallbackCatalog()); + } + List result = new ArrayList<>(); + for (Object entry : (List) pluginsObj) { + if (!(entry instanceof Map)) { + continue; + } + Map m = (Map) entry; + String artifactId = stringVal(m.get("artifactId")); + if (StringUtils.isBlank(artifactId)) { + continue; + } + result.add( + new OptionalPluginInfo( + artifactId, + stringVal(m.get("name")), + stringVal(m.get("category")), + stringVal(m.get("description")), + stringVal(m.get("installPath")))); + } + if (result.isEmpty()) { + return Collections.unmodifiableList(fallbackCatalog()); + } + return Collections.unmodifiableList(result); + } catch (Exception e) { + return Collections.unmodifiableList(fallbackCatalog()); + } + } + + private static String stringVal(Object o) { + return o == null ? null : String.valueOf(o); + } + + /** Hard-coded fallback if the registry resource is missing (tests / broken packaging). */ + private static List fallbackCatalog() { + List wave1 = new ArrayList<>(); + wave1.add( + new OptionalPluginInfo( + "hop-engines-spark", + "Native Spark engine", + "Engines", + "Run pipelines on Apache Spark (local or cluster).", + "plugins/engines/spark")); + wave1.add( + new OptionalPluginInfo( + "hop-engines-beam", + "Apache Beam engine", + "Engines", + "Beam pipeline engine and transforms (includes Beam SDKs under lib-beam).", + "plugins/engines/beam")); + wave1.add( + new OptionalPluginInfo( + "hop-transform-script", + "Script transform", + "Transforms", + "Scripting transform with embedded language runtimes.", + "plugins/transforms/script")); + wave1.add( + new OptionalPluginInfo( + "hop-tech-cassandra", + "Cassandra", + "Technology", + "Apache Cassandra input/output and related tech.", + "plugins/tech/cassandra")); + wave1.add( + new OptionalPluginInfo( + "hop-transform-tika", + "Apache Tika", + "Transforms", + "Extract text and metadata from documents via Apache Tika.", + "plugins/transforms/tika")); + wave1.add( + new OptionalPluginInfo( + "hop-transform-drools", + "Drools", + "Transforms", + "Business rules with Drools.", + "plugins/transforms/drools")); + wave1.add( + new OptionalPluginInfo( + "hop-tech-parquet", + "Parquet", + "Technology", + "Parquet file format support.", + "plugins/tech/parquet")); + wave1.add( + new OptionalPluginInfo( + "hop-transform-stanfordnlp", + "Stanford NLP", + "Transforms", + "Natural language processing with Stanford NLP.", + "plugins/transforms/stanfordnlp")); + wave1.add( + new OptionalPluginInfo( + "hop-tech-arrow", + "Apache Arrow", + "Technology", + "Apache Arrow integration.", + "plugins/tech/arrow")); + wave1.add( + new OptionalPluginInfo( + "hop-tech-dropbox", + "Dropbox", + "Technology", + "Dropbox VFS and related support.", + "plugins/tech/dropbox")); + wave1.add( + new OptionalPluginInfo( + "hop-transform-edi2xml", + "EDI to XML", + "Transforms", + "Convert EDI messages to XML.", + "plugins/transforms/edi2xml")); + return wave1; + } + + /** Artifact ids only (for tests / tooling). */ + public static List artifactIds() { + List ids = new ArrayList<>(); + for (OptionalPluginInfo info : PLUGINS) { + ids.add(info.getArtifactId()); + } + return ids; + } + + /** Expose registry as simple maps for tests. */ + public static Map installPathsByArtifactId() { + Map map = new LinkedHashMap<>(); + for (OptionalPluginInfo info : PLUGINS) { + map.put(info.getArtifactId(), info.getInstallPath()); + } + return map; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginInfo.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginInfo.java new file mode 100644 index 00000000000..db5d1acac19 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/OptionalPluginInfo.java @@ -0,0 +1,65 @@ +/* + * 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.marketplace.catalog; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OptionalPluginInfo { + private String groupId; + private String artifactId; + private String version; + private String name; + private String category; + private String description; + private String installPath; + + /** Where this entry came from (e.g. {@code apache}, repository id, catalog URL). */ + private String source; + + /** Last modified time from remote repository (ISO-8601 or raw Nexus value), if known. */ + private String lastUpdated; + + /** + * Minimum Hop version this plugin supports (e.g. {@code 2.18.1}). When set, discovery hides the + * plugin if the running Hop version is older. + */ + private String minHopVersion; + + /** + * Optional maximum Hop version this plugin supports. When set, discovery hides the plugin if the + * running Hop version is newer. + */ + private String maxHopVersion; + + public OptionalPluginInfo() { + // Jackson / default + } + + public OptionalPluginInfo( + String artifactId, String name, String category, String description, String installPath) { + this.artifactId = artifactId; + this.name = name; + this.category = category; + this.description = description; + this.installPath = installPath; + this.source = "apache"; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/PluginDiscovery.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/PluginDiscovery.java new file mode 100644 index 00000000000..b6fbdf81103 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/PluginDiscovery.java @@ -0,0 +1,402 @@ +/* + * 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.marketplace.catalog; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.HopVersionProvider; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.resolve.MavenCoordinates; + +/** + * Discovers marketplace plugins for query / GUI / install coordinate resolution. + * + *

Always includes the bundled Apache optional catalog, plus a live listing of + * every enabled repository with {@code browse=true} (Nexus zip search). Optional plugin metadata + * embedded in the repository definition enriches live results; it is not a cache. + */ +public final class PluginDiscovery { + + private PluginDiscovery() {} + + /** + * Resolved install target: Maven coordinates plus an optional preferred repository id (from + * discovery {@code source}) tried first in the install fallback chain. + */ + public record InstallTarget(MavenCoordinates coordinates, String preferredRepoId) {} + + /** + * Full marketplace discovery: bundled Apache optional catalog plus every enabled repository with + * {@code browse=true} (live Nexus zip list or optional catalog URL). + * + * @param filter free-text filter (may be blank) + * @param repoId if non-blank, only that browse repository among remotes (Apache catalog still + * included) + */ + public static List query( + String filter, String repoId, MarketplaceConfig config, ILogChannel log) { + String hopVersion = resolveHopVersion(config); + Map byKey = new LinkedHashMap<>(); + + for (OptionalPluginInfo info : OptionalPluginCatalog.query(filter)) { + if (info == null) { + continue; + } + // Apache optional plugins track the running Hop line — not absolute repo . + if (StringUtils.isBlank(info.getVersion()) && StringUtils.isNotBlank(hopVersion)) { + info.setVersion(hopVersion); + } + if (StringUtils.isBlank(info.getSource())) { + info.setSource("apache"); + } + byKey.putIfAbsent(key(info), info); + } + + if (config != null && config.getRepositories() != null) { + for (MarketplaceRepository repo : config.getRepositories()) { + if (repo == null || !repo.isEnabled() || !repo.isBrowse()) { + continue; + } + if (StringUtils.isNotBlank(repoId) && !repoId.equals(repo.getId())) { + continue; + } + try { + for (OptionalPluginInfo info : discoverRepoLive(repo, filter, log)) { + if (info == null) { + continue; + } + if (!VersionCompat.isCompatibleWithHop(info, hopVersion)) { + continue; + } + byKey.putIfAbsent(key(info), info); + } + } catch (Exception e) { + if (log != null) { + log.logError( + "Marketplace discovery failed for repository '" + + repo.getId() + + "': " + + e.getMessage(), + e); + } + } + } + } + + return new ArrayList<>(byKey.values()); + } + + /** + * Resolve an install coordinate the same way users search with {@code marketplace query}. + * + *

+ */ + public static InstallTarget resolveInstall( + String spec, + String defaultGroupId, + String defaultVersion, + MarketplaceConfig config, + ILogChannel log) + throws HopException { + if (StringUtils.isBlank(spec)) { + throw new HopException("Plugin coordinate is empty"); + } + String trimmed = spec.trim(); + String[] parts = trimmed.split(":"); + if (parts.length > 3) { + throw new HopException( + "Invalid coordinate '" + + spec + + "'. Use artifactId, artifactId:version, or groupId:artifactId:version"); + } + if (parts.length == 3) { + return new InstallTarget(new MavenCoordinates(parts[0], parts[1], parts[2]), null); + } + + String token = parts[0]; + String userVersion = parts.length == 2 ? parts[1] : null; + + List matches = query(token, null, config, log); + OptionalPluginInfo chosen = pickInstallMatch(token, matches); + if (chosen == null) { + return new InstallTarget( + MavenCoordinates.parse(trimmed, defaultGroupId, defaultVersion), null); + } + + String groupId = + StringUtils.isNotBlank(chosen.getGroupId()) ? chosen.getGroupId() : defaultGroupId; + String artifactId = chosen.getArtifactId(); + String version; + if (StringUtils.isNotBlank(userVersion)) { + version = userVersion; + } else if (StringUtils.isNotBlank(chosen.getVersion())) { + version = chosen.getVersion(); + } else { + version = defaultVersion; + } + if (StringUtils.isBlank(version)) { + throw new HopException( + "No version for '" + + artifactId + + "'. Use artifactId:version or groupId:artifactId:version"); + } + + String preferredRepo = null; + if (StringUtils.isNotBlank(chosen.getSource()) + && !"apache".equalsIgnoreCase(chosen.getSource()) + && config != null + && config.findRepository(chosen.getSource()) != null) { + preferredRepo = chosen.getSource(); + } + + return new InstallTarget(new MavenCoordinates(groupId, artifactId, version), preferredRepo); + } + + /** + * Choose a single install target from discovery matches for {@code token} (artifact id or short + * name). Prefer exact artifact id, then unique artifact-id substring match, then unique overall + * match. Throws when multiple candidates remain. + */ + static OptionalPluginInfo pickInstallMatch(String token, List matches) + throws HopException { + if (matches == null || matches.isEmpty() || StringUtils.isBlank(token)) { + return null; + } + String needle = token.trim().toLowerCase(Locale.ROOT); + + List exactArtifact = new ArrayList<>(); + List artifactContains = new ArrayList<>(); + for (OptionalPluginInfo info : matches) { + if (info == null || StringUtils.isBlank(info.getArtifactId())) { + continue; + } + String aid = info.getArtifactId().toLowerCase(Locale.ROOT); + if (aid.equals(needle)) { + exactArtifact.add(info); + } else if (aid.contains(needle)) { + artifactContains.add(info); + } + } + + if (exactArtifact.size() == 1) { + return exactArtifact.get(0); + } + if (exactArtifact.size() > 1) { + // Same artifact from multiple sources/versions — prefer first (query order) + return exactArtifact.get(0); + } + if (artifactContains.size() == 1) { + return artifactContains.get(0); + } + if (artifactContains.size() > 1) { + throw ambiguous(token, artifactContains); + } + + // Description/name-only hits: allow only when unique + List withArtifact = + matches.stream() + .filter(i -> i != null && StringUtils.isNotBlank(i.getArtifactId())) + .collect(Collectors.toList()); + if (withArtifact.size() == 1) { + return withArtifact.get(0); + } + if (withArtifact.size() > 1) { + throw ambiguous(token, withArtifact); + } + return null; + } + + private static HopException ambiguous(String token, List candidates) { + String list = + candidates.stream() + .map( + i -> + " - " + + nullToEmpty(i.getGroupId()) + + ":" + + i.getArtifactId() + + ":" + + (StringUtils.isNotBlank(i.getVersion()) ? i.getVersion() : "?") + + (StringUtils.isNotBlank(i.getSource()) ? " [" + i.getSource() + "]" : "")) + .collect(Collectors.joining("\n")); + return new HopException( + "Ambiguous install target '" + + token + + "'. Matches:\n" + + list + + "\nUse a full artifactId or groupId:artifactId:version."); + } + + /** + * Live list for one repository: Nexus zip search (or catalog URL), enriched with any plugin + * metadata from the repository definition. + */ + public static List discoverRepoLive( + MarketplaceRepository repo, String filter, ILogChannel log) throws Exception { + if (repo == null) { + return List.of(); + } + + List live; + if (StringUtils.isNotBlank(repo.getCatalogUrl())) { + live = RemotePluginCatalog.load(repo); + live = RemotePluginCatalog.filter(live, repo, filter); + } else { + live = NexusRepositoryBrowser.browse(repo, filter, log); + } + + // If live listing failed empty but definition has explicit plugins, use those (not a cache — + // author-supplied list in the YAML). + if (live.isEmpty() && repo.getPlugins() != null && !repo.getPlugins().isEmpty()) { + return RemotePluginCatalog.filter(repo.getPlugins(), repo, filter); + } + + return enrichWithDefinitionMetadata(live, repo); + } + + /** + * Apply name/description/category/hop-range and optional version pin from definition plugins when + * artifact ids match (groupId optional on either side). + */ + static List enrichWithDefinitionMetadata( + List live, MarketplaceRepository repo) { + if (live == null + || live.isEmpty() + || repo.getPlugins() == null + || repo.getPlugins().isEmpty()) { + return live == null ? List.of() : live; + } + Map byGa = new LinkedHashMap<>(); + Map byArtifact = new LinkedHashMap<>(); + for (OptionalPluginInfo p : repo.getPlugins()) { + if (p == null || StringUtils.isBlank(p.getArtifactId())) { + continue; + } + byGa.putIfAbsent(artifactKey(p), p); + byArtifact.putIfAbsent(p.getArtifactId().toLowerCase(Locale.ROOT), p); + } + for (OptionalPluginInfo info : live) { + if (info == null || StringUtils.isBlank(info.getArtifactId())) { + continue; + } + OptionalPluginInfo m = byGa.get(artifactKey(info)); + if (m == null) { + m = byArtifact.get(info.getArtifactId().toLowerCase(Locale.ROOT)); + } + if (m == null) { + continue; + } + if (StringUtils.isNotBlank(m.getName()) + && (StringUtils.isBlank(info.getName()) || info.getName().equals(info.getArtifactId()))) { + info.setName(m.getName()); + } + if (StringUtils.isNotBlank(m.getDescription()) + && (StringUtils.isBlank(info.getDescription()) + || "auto-discovered".equals(info.getCategory()) + || looksLikeMavenPath(info.getDescription()))) { + if (!looksLikeMavenPath(m.getDescription())) { + info.setDescription(m.getDescription()); + } + } + // Prefer human category from YAML over Nexus auto-discovered + if (StringUtils.isNotBlank(m.getCategory()) + && (StringUtils.isBlank(info.getCategory()) + || "auto-discovered".equalsIgnoreCase(info.getCategory()) + || !"auto-discovered".equalsIgnoreCase(m.getCategory()))) { + if (!"auto-discovered".equalsIgnoreCase(m.getCategory()) + || StringUtils.isBlank(info.getCategory())) { + info.setCategory(m.getCategory()); + } + } + if (StringUtils.isNotBlank(m.getMinHopVersion())) { + info.setMinHopVersion(m.getMinHopVersion()); + } + if (StringUtils.isNotBlank(m.getMaxHopVersion())) { + info.setMaxHopVersion(m.getMaxHopVersion()); + } + // Optional pin of plugin artifact version from the shareable definition + if (StringUtils.isNotBlank(m.getVersion())) { + info.setVersion(m.getVersion()); + } + if (StringUtils.isBlank(info.getGroupId()) && StringUtils.isNotBlank(m.getGroupId())) { + info.setGroupId(m.getGroupId()); + } + } + return live; + } + + private static boolean looksLikeMavenPath(String s) { + return s != null && s.contains("/") && s.contains("."); + } + + static String artifactKey(OptionalPluginInfo info) { + String g = + StringUtils.isNotBlank(info.getGroupId()) + ? info.getGroupId().toLowerCase(Locale.ROOT) + : "org.apache.hop"; + return g + ":" + info.getArtifactId().toLowerCase(Locale.ROOT); + } + + private static String key(OptionalPluginInfo info) { + return nullToEmpty(info.getGroupId()) + + ":" + + nullToEmpty(info.getArtifactId()) + + ":" + + nullToEmpty(info.getVersion()) + + ":" + + nullToEmpty(info.getSource()); + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + + /** + * Running Hop / marketplace default version — used for Apache optional plugin coordinates and + * minHopVersion checks. Mirrors {@code MarketplaceCommand.resolveDefaultVersion} without a + * circular dependency on the CLI class. + */ + static String resolveHopVersion(MarketplaceConfig config) { + if (config != null && StringUtils.isNotBlank(config.getDefaultVersion())) { + return config.getDefaultVersion(); + } + try { + String[] versions = new HopVersionProvider().getVersion(); + if (versions != null && versions.length > 0 && StringUtils.isNotBlank(versions[0])) { + return versions[0]; + } + } catch (Exception ignored) { + // fall through + } + return System.getProperty("hop.version", "2.19.0-SNAPSHOT"); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/RemotePluginCatalog.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/RemotePluginCatalog.java new file mode 100644 index 00000000000..61dc658859c --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/RemotePluginCatalog.java @@ -0,0 +1,214 @@ +/* + * 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.marketplace.catalog; + +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** Loads a remote or local hop-marketplace-catalog YAML/JSON. */ +public final class RemotePluginCatalog { + + private RemotePluginCatalog() {} + + public static List load(MarketplaceRepository repository) + throws HopException { + if (repository == null || StringUtils.isBlank(repository.getCatalogUrl())) { + return List.of(); + } + String url = repository.getCatalogUrl().trim(); + List plugins; + if (url.startsWith("http://") || url.startsWith("https://")) { + plugins = loadHttp(url); + } else { + plugins = loadFile(Path.of(url)); + } + String source = + StringUtils.isNotBlank(repository.getId()) ? repository.getId() : repository.displayName(); + for (OptionalPluginInfo p : plugins) { + if (p != null && StringUtils.isBlank(p.getSource())) { + p.setSource(source); + } + if (p != null && StringUtils.isBlank(p.getGroupId())) { + p.setGroupId("org.apache.hop"); + } + } + return filter(plugins, repository, null); + } + + public static List loadFile(Path file) throws HopException { + if (file == null || !Files.isRegularFile(file)) { + throw new HopException("Catalog file not found: " + file); + } + try (InputStream in = Files.newInputStream(file)) { + return parse(in, file.toString()); + } catch (Exception e) { + throw new HopException("Unable to read catalog: " + file, e); + } + } + + public static List loadHttp(String url) throws HopException { + try { + HttpClient client = + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + HttpRequest request = + HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(60)).GET().build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new HopException( + "Unable to download catalog from " + url + " (HTTP " + response.statusCode() + ")"); + } + try (InputStream in = response.body()) { + return parse(in, url); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to download catalog from " + url, e); + } + } + + @SuppressWarnings("unchecked") + static List parse(InputStream in, String source) throws HopException { + try { + String lower = source == null ? "" : source.toLowerCase(Locale.ROOT); + Map root; + if (lower.endsWith(".json")) { + root = HopJson.newMapper().readValue(in, Map.class); + } else { + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + Object loaded = yaml.load(in); + if (!(loaded instanceof Map)) { + throw new HopException("Catalog root must be a mapping: " + source); + } + root = (Map) loaded; + } + Object pluginsNode = root.get("plugins"); + if (!(pluginsNode instanceof List list)) { + return List.of(); + } + List out = new ArrayList<>(); + for (Object item : list) { + if (!(item instanceof Map m)) { + continue; + } + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setGroupId(str(m.get("groupId"))); + info.setArtifactId(str(m.get("artifactId"))); + info.setVersion(str(m.get("version"))); + info.setName(str(m.get("name"))); + info.setCategory(str(m.get("category"))); + info.setDescription(str(m.get("description"))); + info.setInstallPath(str(m.get("installPath"))); + info.setMinHopVersion(str(m.get("minHopVersion"))); + info.setMaxHopVersion(str(m.get("maxHopVersion"))); + info.setSource(str(m.get("source"))); + if (StringUtils.isNotBlank(info.getArtifactId())) { + out.add(info); + } + } + return out; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to parse catalog: " + source, e); + } + } + + /** Apply repository filters and optional free-text filter. */ + public static List filter( + List plugins, MarketplaceRepository repository, String textFilter) { + if (plugins == null || plugins.isEmpty()) { + return List.of(); + } + String groupFilter = + repository != null ? StringUtils.trimToNull(repository.getGroupIdFilter()) : null; + boolean includeSnapshots = repository == null || repository.isIncludeSnapshots(); + String repoQuery = + repository != null ? StringUtils.trimToNull(repository.getSearchQuery()) : null; + String text = StringUtils.trimToNull(textFilter); + List out = new ArrayList<>(); + for (OptionalPluginInfo p : plugins) { + if (p == null || StringUtils.isBlank(p.getArtifactId())) { + continue; + } + if (groupFilter != null + && StringUtils.isNotBlank(p.getGroupId()) + && !groupFilter.equals(p.getGroupId())) { + continue; + } + if (!includeSnapshots + && p.getVersion() != null + && org.apache.hop.marketplace.resolve.SnapshotVersions.isSnapshot(p.getVersion())) { + continue; + } + if (repoQuery != null && !matchesText(p, repoQuery)) { + continue; + } + if (text != null && !matchesText(p, text)) { + continue; + } + out.add(p); + } + return out; + } + + static boolean matchesText(OptionalPluginInfo p, String needle) { + String n = needle.toLowerCase(Locale.ROOT); + return contains(p.getArtifactId(), n) + || contains(p.getName(), n) + || contains(p.getCategory(), n) + || contains(p.getDescription(), n) + || contains(p.getGroupId(), n) + || contains(p.getVersion(), n) + || contains(p.getInstallPath(), n) + || contains(p.getSource(), n); + } + + private static boolean contains(String value, String needleLower) { + return value != null && value.toLowerCase(Locale.ROOT).contains(needleLower); + } + + private static String str(Object o) { + if (o == null) { + return null; + } + String s = String.valueOf(o).trim(); + return s.isEmpty() ? null : s; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/VersionCompat.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/VersionCompat.java new file mode 100644 index 00000000000..681fb3ec266 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/catalog/VersionCompat.java @@ -0,0 +1,160 @@ +/* + * 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.marketplace.catalog; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import org.apache.commons.lang3.StringUtils; + +/** + * Hop / plugin version comparison and compatibility checks for marketplace discovery. + * + *

Lightweight Maven-like ordering: numeric segments, then qualifiers; {@code -SNAPSHOT} is + * treated as a pre-release of the same base version. + */ +public final class VersionCompat { + + private VersionCompat() {} + + /** + * Whether {@code info} may be offered on the given running Hop version. Blank min/max means no + * bound on that side. Missing hop version fails closed only when a bound is present. + */ + public static boolean isCompatibleWithHop(OptionalPluginInfo info, String hopVersion) { + if (info == null) { + return false; + } + String min = info.getMinHopVersion(); + String max = info.getMaxHopVersion(); + if (StringUtils.isBlank(min) && StringUtils.isBlank(max)) { + return true; + } + if (StringUtils.isBlank(hopVersion)) { + return false; + } + if (StringUtils.isNotBlank(min) && compare(hopVersion, min) < 0) { + return false; + } + if (StringUtils.isNotBlank(max) && compare(hopVersion, max) > 0) { + return false; + } + return true; + } + + /** + * @return negative if a < b, zero if equal, positive if a > b + */ + public static int compare(String a, String b) { + List pa = parse(a); + List pb = parse(b); + int n = Math.max(pa.size(), pb.size()); + for (int i = 0; i < n; i++) { + Object xa = i < pa.size() ? pa.get(i) : 0; + Object xb = i < pb.size() ? pb.get(i) : 0; + int c = comparePart(xa, xb); + if (c != 0) { + return c; + } + } + return 0; + } + + /** Highest version string in {@code versions}, or null if empty. */ + public static String latest(Collection versions) { + if (versions == null || versions.isEmpty()) { + return null; + } + return versions.stream() + .filter(StringUtils::isNotBlank) + .max(VersionCompat::compare) + .orElse(null); + } + + /** Comparator for plugin artifact versions (newest first). */ + public static Comparator newestFirst() { + return (a, b) -> compare(b, a); + } + + private static int comparePart(Object a, Object b) { + boolean aNum = a instanceof Integer; + boolean bNum = b instanceof Integer; + if (aNum && bNum) { + return Integer.compare((Integer) a, (Integer) b); + } + if (aNum) { + // numeric segment > qualifier (1.0 > 1.0-alpha) + return 1; + } + if (bNum) { + return -1; + } + String sa = String.valueOf(a).toLowerCase(Locale.ROOT); + String sb = String.valueOf(b).toLowerCase(Locale.ROOT); + // snapshot is older than release of same base when compared as qualifier after base + int rankA = qualifierRank(sa); + int rankB = qualifierRank(sb); + if (rankA != rankB) { + return Integer.compare(rankA, rankB); + } + return sa.compareTo(sb); + } + + private static int qualifierRank(String q) { + if ("snapshot".equals(q)) { + return -1; + } + if ("final".equals(q) || "ga".equals(q) || "release".equals(q) || q.isEmpty()) { + return 1; + } + return 0; + } + + static List parse(String version) { + List parts = new ArrayList<>(); + if (StringUtils.isBlank(version)) { + return parts; + } + String v = version.trim(); + // Split on . - and treat SNAPSHOT specially + StringBuilder buf = new StringBuilder(); + for (int i = 0; i <= v.length(); i++) { + char ch = i < v.length() ? v.charAt(i) : '.'; + if (ch == '.' || ch == '-' || i == v.length()) { + if (buf.length() > 0) { + String token = buf.toString(); + buf.setLength(0); + if (token.chars().allMatch(Character::isDigit)) { + try { + parts.add(Integer.parseInt(token)); + } catch (NumberFormatException e) { + parts.add(token.toLowerCase(Locale.ROOT)); + } + } else { + parts.add(token.toLowerCase(Locale.ROOT)); + } + } + } else { + buf.append(ch); + } + } + return parts; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/CliTable.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/CliTable.java new file mode 100644 index 00000000000..4f0acd84490 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/CliTable.java @@ -0,0 +1,169 @@ +/* + * 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.marketplace.command; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +/** Portable ASCII table and CSV printers for marketplace CLI output. */ +public final class CliTable { + + private CliTable() {} + + /** + * Print a bordered ASCII table. Column widths are the max of header and cell content (after + * optional truncation of individual cells by the caller). + */ + public static void printTable(PrintStream out, List headers, List> rows) { + if (headers == null || headers.isEmpty()) { + return; + } + int cols = headers.size(); + int[] widths = new int[cols]; + for (int c = 0; c < cols; c++) { + widths[c] = displayWidth(headers.get(c)); + } + if (rows != null) { + for (List row : rows) { + if (row == null) { + continue; + } + for (int c = 0; c < cols; c++) { + String cell = c < row.size() ? nullToEmpty(row.get(c)) : ""; + widths[c] = Math.max(widths[c], displayWidth(cell)); + } + } + } + + String separator = buildSeparator(widths); + out.println(separator); + out.println(buildRow(headers, widths)); + out.println(separator); + if (rows != null) { + for (List row : rows) { + if (row == null) { + continue; + } + List padded = new ArrayList<>(cols); + for (int c = 0; c < cols; c++) { + padded.add(c < row.size() ? nullToEmpty(row.get(c)) : ""); + } + out.println(buildRow(padded, widths)); + } + } + out.println(separator); + } + + /** Print CSV with a header row. Fields are quoted when needed (comma, quote, CR/LF). */ + public static void printCsv(PrintStream out, List headers, List> rows) { + if (headers == null || headers.isEmpty()) { + return; + } + out.println(toCsvLine(headers)); + if (rows == null) { + return; + } + int cols = headers.size(); + for (List row : rows) { + if (row == null) { + continue; + } + List cells = new ArrayList<>(cols); + for (int c = 0; c < cols; c++) { + cells.add(c < row.size() ? nullToEmpty(row.get(c)) : ""); + } + out.println(toCsvLine(cells)); + } + } + + static String toCsvLine(List fields) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + sb.append(','); + } + sb.append(escapeCsv(fields.get(i))); + } + return sb.toString(); + } + + static String escapeCsv(String value) { + String v = nullToEmpty(value); + boolean needsQuotes = + v.indexOf(',') >= 0 || v.indexOf('"') >= 0 || v.indexOf('\n') >= 0 || v.indexOf('\r') >= 0; + if (!needsQuotes) { + return v; + } + return '"' + v.replace("\"", "\"\"") + '"'; + } + + /** + * Truncate for display, appending {@code ...} when shortened. {@code maxLen} is the maximum + * output length including the ellipsis. + */ + public static String truncate(String value, int maxLen) { + String v = nullToEmpty(value).replace('\n', ' ').replace('\r', ' '); + if (maxLen <= 0 || v.length() <= maxLen) { + return v; + } + if (maxLen <= 3) { + return v.substring(0, maxLen); + } + return v.substring(0, maxLen - 3) + "..."; + } + + private static String buildSeparator(int[] widths) { + StringBuilder sb = new StringBuilder(); + sb.append('+'); + for (int w : widths) { + sb.append("-".repeat(w + 2)); + sb.append('+'); + } + return sb.toString(); + } + + private static String buildRow(List cells, int[] widths) { + StringBuilder sb = new StringBuilder(); + sb.append('|'); + for (int c = 0; c < widths.length; c++) { + String cell = c < cells.size() ? nullToEmpty(cells.get(c)) : ""; + sb.append(' '); + sb.append(padRight(cell, widths[c])); + sb.append(' '); + sb.append('|'); + } + return sb.toString(); + } + + private static String padRight(String s, int width) { + int pad = width - displayWidth(s); + if (pad <= 0) { + return s; + } + return s + " ".repeat(pad); + } + + private static int displayWidth(String s) { + return nullToEmpty(s).length(); + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java new file mode 100644 index 00000000000..0eaeca2ee2a --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/command/MarketplaceCommand.java @@ -0,0 +1,872 @@ +/* + * 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.marketplace.command; + +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.HopVersionProvider; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.hop.plugin.HopCommand; +import org.apache.hop.hop.plugin.IHopCommand; +import org.apache.hop.marketplace.catalog.OptionalPluginCatalog; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.marketplace.catalog.PluginDiscovery; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.config.MarketplaceRepositoryDefinition; +import org.apache.hop.marketplace.env.EnvironmentApplier; +import org.apache.hop.marketplace.env.EnvironmentDrift; +import org.apache.hop.marketplace.env.HopEnvironmentLoader; +import org.apache.hop.marketplace.env.HopEnvironmentSpec; +import org.apache.hop.marketplace.install.HopHome; +import org.apache.hop.marketplace.install.InstallReceipt; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.apache.hop.marketplace.install.PluginUninstaller; +import org.apache.hop.marketplace.resolve.MavenCoordinates; +import org.apache.hop.metadata.api.IHasHopMetadataProvider; +import org.apache.hop.metadata.serializer.multi.MultiMetadataProvider; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Getter +@Setter +@Command( + name = "marketplace", + mixinStandardHelpOptions = true, + description = "Install and manage optional Hop plugins from a Maven repository", + subcommands = { + MarketplaceCommand.InstallCommand.class, + MarketplaceCommand.UninstallCommand.class, + MarketplaceCommand.ListCommand.class, + MarketplaceCommand.QueryCommand.class, + MarketplaceCommand.ApplyCommand.class, + MarketplaceCommand.ValidateCommand.class, + MarketplaceCommand.RepoCommand.class + }) +@HopCommand(id = "marketplace", description = "Hop plugin marketplace") +public class MarketplaceCommand implements Runnable, IHopCommand, IHasHopMetadataProvider { + + private ILogChannel log; + private CommandLine cmd; + private IVariables variables; + private MultiMetadataProvider metadataProvider; + + @Override + public void initialize( + CommandLine cmd, IVariables variables, MultiMetadataProvider metadataProvider) + throws HopException { + this.cmd = cmd; + this.variables = variables; + this.metadataProvider = metadataProvider; + this.log = new LogChannel("Marketplace"); + // Wire nested subcommands (including marketplace repo *) with shared context + wireSubcommands(cmd, log, variables); + } + + private static void wireSubcommands( + CommandLine commandLine, ILogChannel log, IVariables variables) { + for (CommandLine sub : commandLine.getSubcommands().values()) { + Object userObject = sub.getCommand(); + if (userObject instanceof MarketplaceSubCommand nested) { + nested.init(log, variables); + } + wireSubcommands(sub, log, variables); + } + } + + @Override + public void run() { + cmd.usage(System.out); + } + + public static String resolveDefaultVersion(MarketplaceConfig config) { + if (StringUtils.isNotBlank(config.getDefaultVersion())) { + return config.getDefaultVersion(); + } + String[] versions = new HopVersionProvider().getVersion(); + if (versions != null && versions.length > 0 && StringUtils.isNotBlank(versions[0])) { + return versions[0]; + } + // Development builds often have null Implementation-Version + return System.getProperty("hop.version", "2.19.0-SNAPSHOT"); + } + + abstract static class MarketplaceSubCommand implements Runnable { + protected ILogChannel log; + protected IVariables variables; + + void init(ILogChannel log, IVariables variables) { + this.log = log; + this.variables = variables; + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "install", + description = + "Download and install a plugin zip. Short names resolve via the same discovery as query" + + " (e.g. datavault → hop-datavault:0.4.0-SNAPSHOT from a browse repo).") + static class InstallCommand extends MarketplaceSubCommand { + @Parameters( + index = "0", + paramLabel = "COORDINATE", + description = + "Short name, artifactId, artifactId:version, or groupId:artifactId:version" + + " (e.g. datavault, hop-tech-parquet, hop-datavault:0.4.0-SNAPSHOT)") + private String coordinate; + + @Option( + names = {"--repo"}, + description = + "Use only this repository id (skip fallback chain). Default: prefer discovery source," + + " then primary and other enabled repos.") + private String repoId; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + if (!config.isEnabled()) { + throw new HopException("Marketplace is disabled in hop-config.json"); + } + Path hopHome = HopHome.resolve(); + // Activate any previously staged plugins first + new PluginInstaller(log, hopHome, config).activateAllPending(); + + PluginDiscovery.InstallTarget target = + PluginDiscovery.resolveInstall( + coordinate, config.getGroupId(), resolveDefaultVersion(config), config, log); + MavenCoordinates gav = target.coordinates(); + if (StringUtils.isNotBlank(target.preferredRepoId()) && StringUtils.isBlank(repoId)) { + System.out.println( + "Resolved " + + coordinate + + " → " + + gav.gav() + + " (prefer repo '" + + target.preferredRepoId() + + "')"); + } else if (!coordinate.trim().equals(gav.artifactId()) || !coordinate.contains(":")) { + System.out.println("Resolved " + coordinate + " → " + gav.gav()); + } + InstallReceipt receipt = + new PluginInstaller(log, hopHome, config) + .install(gav, true, repoId, target.preferredRepoId()); + System.out.println( + "Plugin " + + gav.gav() + + " installed under " + + hopHome + + (receipt.getRepositoryId() != null + ? " from repo '" + receipt.getRepositoryId() + "'" + : "") + + ". Restart Hop to load it."); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + if (log != null) { + log.logError("Marketplace install failed", e); + } + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "install failed" : e.getMessage(), e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "uninstall", + description = "Remove a plugin previously installed via the marketplace") + static class UninstallCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ARTIFACT_ID", description = "e.g. hop-tech-parquet") + private String artifactId; + + @Override + public void run() { + try { + Path hopHome = HopHome.resolve(); + new PluginUninstaller(log, hopHome).uninstall(artifactId); + System.out.println("Uninstalled " + artifactId + ". Restart Hop."); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "uninstall failed" : e.getMessage(), e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "list", + description = "List marketplace-installed plugins (receipts)") + static class ListCommand extends MarketplaceSubCommand { + @Override + public void run() { + try { + Path hopHome = HopHome.resolve(); + Path receipts = hopHome.resolve(PluginInstaller.RECEIPTS_DIR); + if (!Files.isDirectory(receipts)) { + System.out.println("No marketplace-installed plugins."); + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(receipts, "*.json")) { + boolean any = false; + for (Path file : stream) { + any = true; + InstallReceipt receipt = + HopJson.newMapper().readValue(file.toFile(), InstallReceipt.class); + System.out.printf( + "%s %s:%s:%s%s%n", + receipt.getArtifactId(), + receipt.getGroupId(), + receipt.getArtifactId(), + receipt.getVersion(), + receipt.isPendingActivation() ? " (pending activation)" : ""); + } + if (!any) { + System.out.println("No marketplace-installed plugins."); + } + } + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "list failed" : e.getMessage(), e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "query", + description = + "Search plugins: bundled Apache optional catalog plus every enabled repository with" + + " browse=true (live Nexus zip list). Filter is a case-insensitive substring." + + " Default output is an ASCII table; use --csv for machine-readable CSV.") + static class QueryCommand extends MarketplaceSubCommand { + private static final int TABLE_DESCRIPTION_MAX = 56; + + @Parameters( + index = "0", + arity = "0..1", + paramLabel = "FILTER", + description = + "Case-insensitive substring to match. Omit or use empty string to list matches.") + private String filter; + + @Option( + names = {"--repo"}, + description = + "Only include this repository id among browse-enabled remotes (Apache catalog still" + + " included).") + private String repoId; + + @Option( + names = {"--csv"}, + description = "Print results as CSV (header + rows) instead of an ASCII table") + private boolean csv; + + @Option( + names = {"--include-gav"}, + description = "Include a GAV (groupId:artifactId:version) column in the output") + private boolean includeGav; + + @Override + public void run() { + try { + Path hopHome = null; + try { + hopHome = HopHome.resolve(); + } catch (Exception ignored) { + // installed status is optional + } + MarketplaceConfig config = MarketplaceConfig.load(); + List matches = PluginDiscovery.query(filter, repoId, config, log); + + List headers = queryHeaders(includeGav); + List> rows = new ArrayList<>(); + for (OptionalPluginInfo info : matches) { + rows.add(queryRow(info, hopHome, includeGav, csv)); + } + + if (csv) { + // Always emit header so scripts get stable columns even when empty + CliTable.printCsv(System.out, headers, rows); + return; + } + + if (matches.isEmpty()) { + System.out.println( + "No plugins match filter: " + (StringUtils.isBlank(filter) ? "(all)" : filter)); + return; + } + CliTable.printTable(System.out, headers, rows); + System.out.println(matches.size() + " plugin(s) matched."); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "query failed" : e.getMessage(), e); + } + } + + static List queryHeaders(boolean includeGav) { + List headers = new ArrayList<>(); + headers.add("ARTIFACT"); + headers.add("VERSION"); + headers.add("CATEGORY"); + headers.add("SOURCE"); + if (includeGav) { + headers.add("GAV"); + } + headers.add("INSTALLED"); + headers.add("UPDATED"); + headers.add("DESCRIPTION"); + return headers; + } + + static List queryRow( + OptionalPluginInfo info, Path hopHome, boolean includeGav, boolean fullDescription) { + List row = new ArrayList<>(); + row.add(Const.NVL(info.getArtifactId(), "")); + row.add(Const.NVL(info.getVersion(), "")); + row.add(Const.NVL(info.getCategory(), "")); + row.add(Const.NVL(info.getSource(), "apache")); + if (includeGav) { + row.add(formatGav(info)); + } + boolean onDisk = hopHome != null && OptionalPluginCatalog.isInstalledOnDisk(hopHome, info); + row.add(onDisk ? "yes" : ""); + row.add(Const.NVL(info.getLastUpdated(), "")); + String description = Const.NVL(info.getDescription(), ""); + if (!fullDescription) { + description = CliTable.truncate(description, TABLE_DESCRIPTION_MAX); + } + row.add(description); + return row; + } + + static String formatGav(OptionalPluginInfo info) { + String group = + StringUtils.isNotBlank(info.getGroupId()) ? info.getGroupId() : "org.apache.hop"; + String artifact = Const.NVL(info.getArtifactId(), ""); + String version = StringUtils.isNotBlank(info.getVersion()) ? info.getVersion() : "?"; + return group + ":" + artifact + ":" + version; + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "apply", + description = + "Install plugins and dependencies declared in hop-env.yaml (or .json). Optional --prune" + + " removes marketplace plugins not listed in the file.") + static class ApplyCommand extends MarketplaceSubCommand { + @Option( + names = {"-f", "--file"}, + required = true, + description = "Path to hop-env.yaml or hop-env.json") + private String file; + + @Option( + names = {"--prune"}, + description = + "Uninstall marketplace-installed plugins that are not listed in the environment file") + private boolean prune; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + if (!config.isEnabled()) { + throw new HopException("Marketplace is disabled in hop-config.json"); + } + Path hopHome = HopHome.resolve(); + Path envPath = Path.of(file).toAbsolutePath().normalize(); + HopEnvironmentSpec env = HopEnvironmentLoader.load(envPath); + new EnvironmentApplier(log, hopHome, config).apply(env, prune); + System.out.println( + "Environment applied from " + + envPath + + ". Restart Hop (or re-run) so new plugins are loaded."); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + if (log != null) { + log.logError("Marketplace apply failed", e); + } + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "apply failed" : e.getMessage(), e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "validate", + description = + "Check the local install against hop-env.yaml without installing. Exit code 1 on drift.") + static class ValidateCommand extends MarketplaceSubCommand { + @Option( + names = {"-f", "--file"}, + description = + "Path to hop-env.yaml or hop-env.json (default: discover hop-env.* under project/Hop" + + " home)") + private String file; + + @Option( + names = {"--strict"}, + description = "Also fail if extra marketplace plugins are installed beyond the env file") + private boolean strict; + + @Override + public void run() { + try { + Path hopHome = HopHome.resolve(); + Path envPath = EnvironmentApplier.resolveEnvironmentFile(hopHome, file); + if (envPath == null) { + throw new HopException( + "No environment file found. Pass -f hop-env.yaml or set HOP_ENV_FILE."); + } + HopEnvironmentSpec env = HopEnvironmentLoader.load(envPath); + EnvironmentDrift drift = + new EnvironmentApplier(log, hopHome, MarketplaceConfig.load()).validate(env); + boolean hard = + !drift.getMissingPlugins().isEmpty() + || !drift.getVersionMismatches().isEmpty() + || !drift.getMissingDependencies().isEmpty() + || (strict && !drift.getExtraMarketplacePlugins().isEmpty()); + // populate extras only when strict (validate() currently does not; add here) + if (strict) { + addExtraPlugins(hopHome, env, drift); + hard = hard || !drift.getExtraMarketplacePlugins().isEmpty(); + } + if (!hard) { + System.out.println("OK: environment matches " + envPath); + return; + } + System.err.println("Environment drift against " + envPath + ":"); + System.err.print(drift.formatReport()); + System.err.println("Run: hop marketplace apply -f " + envPath); + throw new CommandLine.ExecutionException(new CommandLine(this), "environment drift"); + } catch (CommandLine.ExecutionException e) { + throw e; + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "validate failed" : e.getMessage(), e); + } + } + + private static void addExtraPlugins( + Path hopHome, HopEnvironmentSpec env, EnvironmentDrift drift) throws Exception { + Set desired = new HashSet<>(); + if (env.getPlugins() != null) { + for (HopEnvironmentSpec.PluginRef ref : env.getPlugins()) { + if (ref.getArtifactId() != null) { + desired.add(ref.getArtifactId()); + } + } + } + Path receipts = hopHome.resolve(PluginInstaller.RECEIPTS_DIR); + if (!Files.isDirectory(receipts)) { + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(receipts, "*.json")) { + for (Path f : stream) { + String name = f.getFileName().toString(); + String id = name.substring(0, name.length() - ".json".length()); + if (!desired.contains(id)) { + drift.getExtraMarketplacePlugins().add(id); + } + } + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "repo", + description = "Manage marketplace Maven repositories in hop-config.json", + subcommands = { + MarketplaceCommand.RepoListCommand.class, + MarketplaceCommand.RepoAddCommand.class, + MarketplaceCommand.RepoRemoveCommand.class, + MarketplaceCommand.RepoSetPrimaryCommand.class, + MarketplaceCommand.RepoEnableCommand.class, + MarketplaceCommand.RepoDisableCommand.class, + MarketplaceCommand.RepoResetDefaultsCommand.class, + MarketplaceCommand.RepoExportCommand.class, + MarketplaceCommand.RepoImportCommand.class + }) + static class RepoCommand extends MarketplaceSubCommand { + @Override + public void run() { + // picocli shows usage when no subcommand + new CommandLine(this).usage(System.out); + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "list", + description = "List configured marketplace repositories") + static class RepoListCommand extends MarketplaceSubCommand { + @Override + public void run() { + MarketplaceConfig config = MarketplaceConfig.load(); + System.out.printf( + "%-8s %-10s %-8s %-14s %-20s %s%n", "PRIMARY", "ENABLED", "BROWSE", "ID", "NAME", "URL"); + for (MarketplaceRepository repo : config.getRepositories()) { + if (repo == null) { + continue; + } + System.out.printf( + "%-8s %-10s %-8s %-14s %-20s %s%n", + repo.isPrimary() ? "*" : "", + repo.isEnabled() ? "yes" : "no", + repo.isBrowse() ? "yes" : "no", + Const.NVL(repo.getId(), ""), + Const.NVL(repo.displayName(), ""), + repo.normalizedUrl()); + if (StringUtils.isNotBlank(repo.getCatalogUrl())) { + System.out.println(" catalog: " + repo.getCatalogUrl()); + } + } + System.out.println(); + System.out.println( + "Install order: " + + config.orderedRepositories().stream() + .map(MarketplaceRepository::getId) + .collect(Collectors.joining(" → "))); + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "add", + description = "Add a marketplace repository and save hop-config.json") + static class RepoAddCommand extends MarketplaceSubCommand { + @Option( + names = {"--id"}, + required = true, + description = "Stable repository id (e.g. local-nexus)") + private String id; + + @Option( + names = {"--url"}, + required = true, + description = "Maven base URL (…/repository/hop-plugins/)") + private String url; + + @Option( + names = {"--name"}, + description = "Display name") + private String name; + + @Option( + names = {"--primary"}, + description = "Make this the primary repository") + private boolean primary; + + @Option( + names = {"--username"}, + description = "Optional Basic auth username") + private String username; + + @Option( + names = {"--password"}, + description = "Optional Basic auth password (prefer env HOP_MARKETPLACE_PASSWORD)") + private String password; + + @Option( + names = {"--browse"}, + description = "Include this repository in marketplace query/GUI discovery") + private boolean browse; + + @Option( + names = {"--catalog-url"}, + description = "Optional hop-marketplace-catalog.yaml URL for discovery") + private String catalogUrl; + + @Option( + names = {"--search-query"}, + description = "Optional discovery filter for this repository") + private String searchQuery; + + @Option( + names = {"--group-id-filter"}, + description = "Optional Maven groupId filter for discovery") + private String groupIdFilter; + + @Option( + names = {"--no-snapshots"}, + description = "Hide SNAPSHOT versions in discovery for this repository") + private boolean noSnapshots; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + MarketplaceRepository repo = + new MarketplaceRepository(id, StringUtils.isNotBlank(name) ? name : id, url, primary); + repo.setUsername(username); + repo.setPassword(password); + repo.setBrowse(browse); + repo.setCatalogUrl(catalogUrl); + repo.setSearchQuery(searchQuery); + repo.setGroupIdFilter(groupIdFilter); + repo.setIncludeSnapshots(!noSnapshots); + config.addRepository(repo); + config.save(); + System.out.println( + "Added repository '" + + id + + "'" + + (primary ? " (primary)" : "") + + (browse ? " (browse)" : "") + + " → " + + repo.normalizedUrl()); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), e.getMessage() == null ? "repo add failed" : e.getMessage(), e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "export", + description = + "Export a repository definition YAML/JSON (shareable; passwords omitted). For company" + + " marketplace onboarding.") + static class RepoExportCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ID", description = "Configured repository id") + private String id; + + @Option( + names = {"-o", "--output"}, + required = true, + description = "Output file (.yaml / .yml / .json)") + private String output; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + MarketplaceRepository repo = config.findRepository(id); + if (repo == null) { + throw new HopException("Unknown repository id: " + id); + } + Path out = Path.of(output); + MarketplaceRepositoryDefinition.save(out, repo); + System.out.println("Wrote repository definition: " + out.toAbsolutePath().normalize()); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo export failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "import", + description = + "Import a hop-marketplace-repo.yaml (or URL) into hop-config.json. Upserts by repository" + + " id.") + static class RepoImportCommand extends MarketplaceSubCommand { + @Parameters( + index = "0", + paramLabel = "FILE_OR_URL", + description = "Path or https URL to hop-marketplace-repo.yaml") + private String fileOrUrl; + + @Option( + names = {"--primary"}, + description = "Make the imported repository primary") + private boolean primary; + + @Override + public void run() { + try { + MarketplaceRepository imported = MarketplaceRepositoryDefinition.loadFromUri(fileOrUrl); + MarketplaceConfig config = MarketplaceConfig.load(); + boolean existed = config.findRepository(imported.getId()) != null; + MarketplaceRepositoryDefinition.applyToConfig(config, imported, primary); + config.save(); + System.out.println( + (existed ? "Updated" : "Imported") + + " repository '" + + imported.getId() + + "' → " + + imported.normalizedUrl() + + (imported.isBrowse() ? " (browse enabled)" : "")); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo import failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "remove", + description = "Remove a marketplace repository by id") + static class RepoRemoveCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ID", description = "Repository id") + private String id; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + config.removeRepository(id); + config.save(); + System.out.println("Removed repository '" + id + "'"); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo remove failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "set-primary", + description = "Set the primary marketplace repository") + static class RepoSetPrimaryCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ID", description = "Repository id") + private String id; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + config.setPrimary(id); + config.save(); + System.out.println("Primary marketplace repository is now '" + id + "'"); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo set-primary failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "enable", + description = "Enable a repository in the fallback chain") + static class RepoEnableCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ID") + private String id; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + config.setEnabled(id, true); + config.save(); + System.out.println("Enabled repository '" + id + "'"); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo enable failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "disable", + description = "Disable a repository (skip in fallback chain)") + static class RepoDisableCommand extends MarketplaceSubCommand { + @Parameters(index = "0", paramLabel = "ID") + private String id; + + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + config.setEnabled(id, false); + config.save(); + System.out.println("Disabled repository '" + id + "'"); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo disable failed" : e.getMessage(), + e); + } + } + } + + @Command( + mixinStandardHelpOptions = true, + name = "reset-defaults", + description = "Reset repositories to ASF primary + Maven Central fallback") + static class RepoResetDefaultsCommand extends MarketplaceSubCommand { + @Override + public void run() { + try { + MarketplaceConfig config = MarketplaceConfig.load(); + config.resetToDefaults(); + config.save(); + System.out.println( + "Marketplace repositories reset to ASF primary + Maven Central fallback."); + for (MarketplaceRepository repo : config.getRepositories()) { + System.out.println( + (repo.isPrimary() ? "* " : " ") + repo.getId() + " " + repo.normalizedUrl()); + } + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + throw new CommandLine.ExecutionException( + new CommandLine(this), + e.getMessage() == null ? "repo reset-defaults failed" : e.getMessage(), + e); + } + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceConfig.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceConfig.java new file mode 100644 index 00000000000..d97964f426f --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceConfig.java @@ -0,0 +1,287 @@ +/* + * 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.marketplace.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.config.HopConfig; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.LogChannel; + +/** + * Marketplace settings stored under the {@code marketplace} key in hop-config.json (or defaults). + * + *

Hop always loads that file from {@code ${HOP_CONFIG_FOLDER}/hop-config.json}. When {@code + * HOP_CONFIG_FOLDER} is unset, it defaults to {@code /config} (with the hop launcher, the + * client install's {@code config/} directory). + */ +@Getter +@Setter +public class MarketplaceConfig { + public static final String CONFIG_KEY = "marketplace"; + public static final String DEFAULT_GROUP_ID = "org.apache.hop"; + + public static final String DEFAULT_ASF_ID = "asf"; + public static final String DEFAULT_ASF_NAME = "Apache Repository"; + public static final String DEFAULT_ASF_URL = + "https://repository.apache.org/content/groups/public/"; + + public static final String DEFAULT_CENTRAL_ID = "central"; + public static final String DEFAULT_CENTRAL_NAME = "Maven Central"; + public static final String DEFAULT_CENTRAL_URL = "https://repo1.maven.org/maven2/"; + + /** + * @deprecated use {@link #DEFAULT_ASF_URL} + */ + @Deprecated public static final String DEFAULT_REPO_URL = DEFAULT_ASF_URL; + + private boolean enabled = true; + private String groupId = DEFAULT_GROUP_ID; + private String defaultVersion; + private List repositories = new ArrayList<>(); + + public MarketplaceConfig() { + repositories.addAll(defaultRepositories()); + } + + /** ASF primary + Maven Central fallback (shipped defaults). */ + public static List defaultRepositories() { + List list = new ArrayList<>(); + list.add(new MarketplaceRepository(DEFAULT_ASF_ID, DEFAULT_ASF_NAME, DEFAULT_ASF_URL, true)); + list.add( + new MarketplaceRepository( + DEFAULT_CENTRAL_ID, DEFAULT_CENTRAL_NAME, DEFAULT_CENTRAL_URL, false)); + return list; + } + + public static MarketplaceConfig load() { + ILogChannel log = new LogChannel("Marketplace"); + try { + Object raw = HopConfig.readOption(CONFIG_KEY); + if (raw == null) { + return new MarketplaceConfig(); + } + MarketplaceConfig config = HopJson.newMapper().convertValue(raw, MarketplaceConfig.class); + if (config.getRepositories() == null || config.getRepositories().isEmpty()) { + config.setRepositories(defaultRepositories()); + } + config.ensureValidPrimary(); + return config; + } catch (Exception e) { + log.logError("Unable to load marketplace config, using defaults", e); + return new MarketplaceConfig(); + } + } + + public void save() { + ensureValidPrimary(); + Map asMap = HopJson.newMapper().convertValue(this, Map.class); + HopConfig.getInstance().saveOption(CONFIG_KEY, asMap); + } + + /** Replace repository list with shipped ASF + Central defaults. */ + public void resetToDefaults() { + repositories = defaultRepositories(); + } + + /** + * Ensure exactly one primary among enabled repos when possible. Keeps the first {@code + * primary=true} entry and clears the rest; if none, marks the first enabled repo. + */ + public void ensureValidPrimary() { + if (repositories == null) { + repositories = new ArrayList<>(); + } + MarketplaceRepository firstPrimary = null; + for (MarketplaceRepository repo : repositories) { + if (repo == null) { + continue; + } + if (repo.isPrimary()) { + if (firstPrimary == null) { + firstPrimary = repo; + } else { + repo.setPrimary(false); + } + } + } + if (firstPrimary == null) { + for (MarketplaceRepository repo : repositories) { + if (repo != null && repo.isEnabled()) { + repo.setPrimary(true); + break; + } + } + } + } + + /** First repository base URL, or ASF public. Always ends with {@code /}. */ + public String primaryRepositoryUrl() { + return primaryRepository().normalizedUrl(); + } + + /** + * Primary repository (preferred for display and tried first on install), or ASF built-in + * defaults. + */ + public MarketplaceRepository primaryRepository() { + ensureValidPrimary(); + if (repositories != null) { + for (MarketplaceRepository repo : repositories) { + if (repo != null + && repo.isPrimary() + && repo.isEnabled() + && StringUtils.isNotBlank(repo.getUrl())) { + return repo; + } + } + for (MarketplaceRepository repo : repositories) { + if (repo != null && repo.isEnabled() && StringUtils.isNotBlank(repo.getUrl())) { + return repo; + } + } + } + return new MarketplaceRepository(DEFAULT_ASF_ID, DEFAULT_ASF_NAME, DEFAULT_ASF_URL, true); + } + + /** + * Enabled repositories in install order: primary first, then remaining list order. Used as the + * fallback chain. + */ + public List orderedRepositories() { + ensureValidPrimary(); + List ordered = new ArrayList<>(); + MarketplaceRepository primary = null; + if (repositories != null) { + for (MarketplaceRepository repo : repositories) { + if (repo != null + && repo.isPrimary() + && repo.isEnabled() + && StringUtils.isNotBlank(repo.getUrl())) { + primary = repo; + break; + } + } + } + if (primary != null) { + ordered.add(primary); + } + if (repositories != null) { + for (MarketplaceRepository repo : repositories) { + if (repo == null || !repo.isEnabled() || StringUtils.isBlank(repo.getUrl())) { + continue; + } + if (primary != null && Objects.equals(primary.getId(), repo.getId()) && primary == repo) { + continue; + } + // skip duplicate of primary by id + if (primary != null + && StringUtils.isNotBlank(primary.getId()) + && primary.getId().equals(repo.getId())) { + continue; + } + ordered.add(repo); + } + } + if (ordered.isEmpty()) { + ordered.add( + new MarketplaceRepository(DEFAULT_ASF_ID, DEFAULT_ASF_NAME, DEFAULT_ASF_URL, true)); + } + return ordered; + } + + public MarketplaceRepository findRepository(String id) { + if (StringUtils.isBlank(id) || repositories == null) { + return null; + } + for (MarketplaceRepository repo : repositories) { + if (repo != null && id.equals(repo.getId())) { + return repo; + } + } + return null; + } + + public void addRepository(MarketplaceRepository repo) throws HopException { + if (repo == null || StringUtils.isBlank(repo.getId()) || StringUtils.isBlank(repo.getUrl())) { + throw new HopException("Repository requires id and url"); + } + if (findRepository(repo.getId()) != null) { + throw new HopException("Repository id already exists: " + repo.getId()); + } + if (repositories == null) { + repositories = new ArrayList<>(); + } + if (repo.isPrimary()) { + clearPrimaryFlags(); + } + repositories.add(repo); + ensureValidPrimary(); + } + + public void removeRepository(String id) throws HopException { + MarketplaceRepository existing = findRepository(id); + if (existing == null) { + throw new HopException("Unknown repository id: " + id); + } + repositories.remove(existing); + ensureValidPrimary(); + } + + public void setPrimary(String id) throws HopException { + MarketplaceRepository repo = findRepository(id); + if (repo == null) { + throw new HopException("Unknown repository id: " + id); + } + if (!repo.isEnabled()) { + throw new HopException("Cannot set disabled repository as primary: " + id); + } + clearPrimaryFlags(); + repo.setPrimary(true); + } + + public void setEnabled(String id, boolean enabled) throws HopException { + MarketplaceRepository repo = findRepository(id); + if (repo == null) { + throw new HopException("Unknown repository id: " + id); + } + repo.setEnabled(enabled); + if (!enabled && repo.isPrimary()) { + repo.setPrimary(false); + } + ensureValidPrimary(); + } + + private void clearPrimaryFlags() { + if (repositories == null) { + return; + } + for (MarketplaceRepository repo : repositories) { + if (repo != null) { + repo.setPrimary(false); + } + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepository.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepository.java new file mode 100644 index 00000000000..648b92f8739 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepository.java @@ -0,0 +1,169 @@ +/* + * 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.marketplace.config; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; + +@Getter +@Setter +public class MarketplaceRepository { + private String id = MarketplaceConfig.DEFAULT_ASF_ID; + private String name; + private String url = MarketplaceConfig.DEFAULT_ASF_URL; + + /** When true, this repository is tried first for installs. */ + private boolean primary; + + /** When false, skipped in the install fallback chain. */ + private boolean enabled = true; + + /** Optional HTTP Basic auth username. */ + private String username; + + /** + * Optional HTTP Basic auth password. Prefer leaving this empty in hop-config.json and setting + * {@code HOP_MARKETPLACE_PASSWORD} instead for private repos. Do not set for anonymous ASF / + * Central / local Nexus. + */ + private String password; + + /** + * When true, include this repository in {@code marketplace query} and the GUI Plugins tab (live + * Nexus zip list, or {@link #catalogUrl} if set). Default false so ASF/Central stay install-only + * endpoints. + */ + private boolean browse; + + /** + * Optional remote catalog index URL (YAML/JSON). Advanced only — prefer live Nexus zip listing + * when {@link #browse} is true. + */ + private String catalogUrl; + + /** + * Optional plugin metadata from a shareable repository definition (import/export). Used to enrich + * live discovery results (names, categories, descriptions) or as a fallback list if live browse + * returns nothing. Not a discovery cache — listing always prefers a live Nexus/catalog fetch. + */ + private List plugins = new ArrayList<>(); + + /** + * Optional search/filter string applied when browsing this repository (substring over GAV / + * catalog fields). Empty means no extra filter beyond {@link #groupIdFilter}. + */ + private String searchQuery; + + /** When false, SNAPSHOT versions are hidden from discovery results. Default true. */ + private boolean includeSnapshots = true; + + /** Optional Maven groupId restriction for discovery (e.g. {@code com.acme.hop}). */ + private String groupIdFilter; + + /** Optional human homepage for this marketplace (documentation only). */ + private String homepage; + + /** Optional human description (documentation / export). */ + private String description; + + public MarketplaceRepository() { + // Jackson + } + + public MarketplaceRepository(String id, String url) { + this.id = id; + this.url = url; + this.name = id; + } + + public MarketplaceRepository(String id, String url, boolean primary) { + this(id, url); + this.primary = primary; + } + + public MarketplaceRepository(String id, String name, String url, boolean primary) { + this.id = id; + this.name = name; + this.url = url; + this.primary = primary; + } + + public MarketplaceRepository(String id, String url, String username, String password) { + this(id, url); + this.username = username; + this.password = password; + } + + public String displayName() { + if (StringUtils.isNotBlank(name)) { + return name; + } + return StringUtils.isNotBlank(id) ? id : normalizedUrl(); + } + + /** Base URL always ending with {@code /}. */ + public String normalizedUrl() { + if (StringUtils.isBlank(url)) { + return MarketplaceConfig.DEFAULT_ASF_URL; + } + return url.endsWith("/") ? url : url + "/"; + } + + /** + * Effective credentials: repository config fields, then {@code HOP_MARKETPLACE_USERNAME} / {@code + * HOP_MARKETPLACE_PASSWORD}. No credentials means anonymous HTTP. + */ + public String effectiveUsername() { + if (StringUtils.isNotBlank(username)) { + return username; + } + String fromEnv = + firstNonBlank( + System.getenv("HOP_MARKETPLACE_USERNAME"), System.getenv("HOP_MARKETPLACE_USER")); + return StringUtils.isNotBlank(fromEnv) ? fromEnv : null; + } + + public String effectivePassword() { + if (StringUtils.isNotBlank(password)) { + return password; + } + String fromEnv = System.getenv("HOP_MARKETPLACE_PASSWORD"); + return StringUtils.isNotBlank(fromEnv) ? fromEnv : null; + } + + public boolean hasCredentials() { + return StringUtils.isNotBlank(effectiveUsername()) + && StringUtils.isNotBlank(effectivePassword()); + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String v : values) { + if (StringUtils.isNotBlank(v)) { + return v; + } + } + return null; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinition.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinition.java new file mode 100644 index 00000000000..625f923f0e4 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinition.java @@ -0,0 +1,374 @@ +/* + * 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.marketplace.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** + * Shareable Hop marketplace repository definition ({@code hop-marketplace-repo.yaml}). Passwords + * are never written on export. + */ +public final class MarketplaceRepositoryDefinition { + + public static final String KIND = "hop-marketplace-repository"; + public static final String SCHEMA_VERSION = "1.0"; + + private MarketplaceRepositoryDefinition() {} + + public static MarketplaceRepository load(Path file) throws HopException { + if (file == null || !Files.isRegularFile(file)) { + throw new HopException("Repository definition not found: " + file); + } + try (InputStream in = Files.newInputStream(file)) { + return parse(in, file.toString()); + } catch (IOException e) { + throw new HopException("Unable to read repository definition: " + file, e); + } + } + + public static MarketplaceRepository loadFromUri(String uriOrPath) throws HopException { + if (StringUtils.isBlank(uriOrPath)) { + throw new HopException("Repository definition path or URL is required"); + } + String s = uriOrPath.trim(); + if (s.startsWith("http://") || s.startsWith("https://")) { + return loadFromHttp(s); + } + return load(Path.of(s)); + } + + public static MarketplaceRepository loadFromHttp(String url) throws HopException { + try { + HttpClient client = + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + HttpRequest request = + HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofSeconds(60)).GET().build(); + HttpResponse response = + client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() < 200 || response.statusCode() >= 300) { + throw new HopException( + "Unable to download repository definition from " + + url + + " (HTTP " + + response.statusCode() + + ")"); + } + try (InputStream in = response.body()) { + return parse(in, url); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to download repository definition from " + url, e); + } + } + + static MarketplaceRepository parse(InputStream in, String source) throws HopException { + try { + String name = source == null ? "" : source.toLowerCase(Locale.ROOT); + if (name.endsWith(".json")) { + Map map = HopJson.newMapper().readValue(in, Map.class); + return fromMap(map); + } + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + Object loaded = yaml.load(in); + if (!(loaded instanceof Map)) { + throw new HopException("Repository definition root must be a mapping: " + source); + } + return fromMap((Map) loaded); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to parse repository definition: " + source, e); + } + } + + static MarketplaceRepository fromMap(Map map) throws HopException { + if (map == null) { + throw new HopException("Empty repository definition"); + } + Object kind = map.get("kind"); + if (kind != null && !KIND.equals(String.valueOf(kind).trim())) { + // allow missing kind for brevity + if (StringUtils.isNotBlank(String.valueOf(kind)) + && !String.valueOf(kind).contains("marketplace")) { + throw new HopException( + "Unexpected kind '" + kind + "' (expected " + KIND + " or omit kind)"); + } + } + MarketplaceRepository repo = new MarketplaceRepository(); + repo.setId(stringVal(map.get("id"))); + repo.setName(stringVal(map.get("name"))); + repo.setUrl(stringVal(map.get("url"))); + if (StringUtils.isBlank(repo.getId()) || StringUtils.isBlank(repo.getUrl())) { + throw new HopException("Repository definition requires id and url"); + } + if (StringUtils.isBlank(repo.getName())) { + repo.setName(repo.getId()); + } + repo.setPrimary(boolVal(map.get("primary"), false)); + repo.setEnabled(boolVal(map.get("enabled"), true)); + repo.setUsername(stringVal(map.get("username"))); + // password intentionally ignored on import unless present (discouraged) + String password = stringVal(map.get("password")); + if (StringUtils.isNotBlank(password)) { + repo.setPassword(password); + } + repo.setBrowse(boolVal(map.get("browse"), false)); + repo.setCatalogUrl(stringVal(map.get("catalogUrl"))); + repo.setSearchQuery(stringVal(map.get("searchQuery"))); + repo.setIncludeSnapshots(boolVal(map.get("includeSnapshots"), true)); + repo.setGroupIdFilter(stringVal(map.get("groupIdFilter"))); + repo.setHomepage(stringVal(map.get("homepage"))); + repo.setDescription(stringVal(map.get("description"))); + repo.setPlugins(parsePlugins(map.get("plugins"), repo.getId())); + return repo; + } + + static List parsePlugins(Object pluginsNode, String defaultSource) { + List out = new ArrayList<>(); + if (!(pluginsNode instanceof List list)) { + return out; + } + for (Object item : list) { + if (!(item instanceof Map m)) { + continue; + } + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setGroupId(stringVal(m.get("groupId"))); + info.setArtifactId(stringVal(m.get("artifactId"))); + info.setVersion(stringVal(m.get("version"))); + info.setName(stringVal(m.get("name"))); + info.setCategory(stringVal(m.get("category"))); + info.setDescription(stringVal(m.get("description"))); + info.setInstallPath(stringVal(m.get("installPath"))); + info.setLastUpdated(stringVal(m.get("lastUpdated"))); + info.setMinHopVersion(stringVal(m.get("minHopVersion"))); + info.setMaxHopVersion(stringVal(m.get("maxHopVersion"))); + info.setSource(stringVal(m.get("source"))); + if (StringUtils.isBlank(info.getSource())) { + info.setSource(defaultSource); + } + if (StringUtils.isBlank(info.getCategory())) { + info.setCategory("auto-discovered"); + } + if (StringUtils.isBlank(info.getName())) { + info.setName(info.getArtifactId()); + } + if (StringUtils.isNotBlank(info.getArtifactId())) { + out.add(info); + } + } + return out; + } + + public static void save(Path file, MarketplaceRepository repo) throws HopException { + if (file == null) { + throw new HopException("Output path is required"); + } + if (repo == null) { + throw new HopException("Repository is required"); + } + try { + Path parent = file.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Map map = toYamlMap(repo, false); + String name = file.getFileName().toString().toLowerCase(Locale.ROOT); + if (name.endsWith(".json")) { + HopJson.newMapper().writerWithDefaultPrettyPrinter().writeValue(file.toFile(), map); + return; + } + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + options.setIndent(2); + Yaml yaml = new Yaml(options); + try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + writer.write("# Hop marketplace repository definition (shareable; no passwords)\n"); + yaml.dump(map, writer); + } + } catch (IOException e) { + throw new HopException("Unable to write repository definition: " + file, e); + } + } + + /** Map for export; passwords omitted unless {@code includePassword} is true. */ + public static Map toYamlMap(MarketplaceRepository repo, boolean includePassword) { + Map root = new LinkedHashMap<>(); + root.put("schemaVersion", SCHEMA_VERSION); + root.put("kind", KIND); + root.put("id", repo.getId()); + if (StringUtils.isNotBlank(repo.getName())) { + root.put("name", repo.getName()); + } + root.put("url", repo.getUrl()); + root.put("primary", repo.isPrimary()); + root.put("enabled", repo.isEnabled()); + if (StringUtils.isNotBlank(repo.getUsername())) { + root.put("username", repo.getUsername()); + } + if (includePassword && StringUtils.isNotBlank(repo.getPassword())) { + root.put("password", repo.getPassword()); + } + root.put("browse", repo.isBrowse()); + if (StringUtils.isNotBlank(repo.getCatalogUrl())) { + root.put("catalogUrl", repo.getCatalogUrl()); + } + if (StringUtils.isNotBlank(repo.getSearchQuery())) { + root.put("searchQuery", repo.getSearchQuery()); + } + root.put("includeSnapshots", repo.isIncludeSnapshots()); + if (StringUtils.isNotBlank(repo.getGroupIdFilter())) { + root.put("groupIdFilter", repo.getGroupIdFilter()); + } + if (StringUtils.isNotBlank(repo.getHomepage())) { + root.put("homepage", repo.getHomepage()); + } + if (StringUtils.isNotBlank(repo.getDescription())) { + root.put("description", repo.getDescription()); + } + if (repo.getPlugins() != null && !repo.getPlugins().isEmpty()) { + List> plugins = new ArrayList<>(); + for (OptionalPluginInfo p : repo.getPlugins()) { + if (p == null || StringUtils.isBlank(p.getArtifactId())) { + continue; + } + Map m = new LinkedHashMap<>(); + if (StringUtils.isNotBlank(p.getGroupId())) { + m.put("groupId", p.getGroupId()); + } + m.put("artifactId", p.getArtifactId()); + if (StringUtils.isNotBlank(p.getVersion())) { + m.put("version", p.getVersion()); + } + if (StringUtils.isNotBlank(p.getName())) { + m.put("name", p.getName()); + } + if (StringUtils.isNotBlank(p.getCategory())) { + m.put("category", p.getCategory()); + } + if (StringUtils.isNotBlank(p.getDescription())) { + m.put("description", p.getDescription()); + } + if (StringUtils.isNotBlank(p.getLastUpdated())) { + m.put("lastUpdated", p.getLastUpdated()); + } + if (StringUtils.isNotBlank(p.getInstallPath())) { + m.put("installPath", p.getInstallPath()); + } + if (StringUtils.isNotBlank(p.getMinHopVersion())) { + m.put("minHopVersion", p.getMinHopVersion()); + } + if (StringUtils.isNotBlank(p.getMaxHopVersion())) { + m.put("maxHopVersion", p.getMaxHopVersion()); + } + plugins.add(m); + } + if (!plugins.isEmpty()) { + root.put("plugins", plugins); + } + } + return root; + } + + public static void applyToConfig( + MarketplaceConfig config, MarketplaceRepository imported, boolean makePrimary) + throws HopException { + if (config == null || imported == null) { + throw new HopException("Config and repository are required"); + } + if (makePrimary) { + imported.setPrimary(true); + } + MarketplaceRepository existing = config.findRepository(imported.getId()); + if (existing == null) { + config.addRepository(imported); + } else { + // Upsert discovery + connection fields; keep password if import omitted it + existing.setName(imported.getName()); + existing.setUrl(imported.getUrl()); + existing.setEnabled(imported.isEnabled()); + if (imported.isPrimary() || makePrimary) { + config.setPrimary(existing.getId()); + } + if (StringUtils.isNotBlank(imported.getUsername())) { + existing.setUsername(imported.getUsername()); + } + if (StringUtils.isNotBlank(imported.getPassword())) { + existing.setPassword(imported.getPassword()); + } + existing.setBrowse(imported.isBrowse()); + existing.setCatalogUrl(imported.getCatalogUrl()); + existing.setSearchQuery(imported.getSearchQuery()); + existing.setIncludeSnapshots(imported.isIncludeSnapshots()); + existing.setGroupIdFilter(imported.getGroupIdFilter()); + existing.setHomepage(imported.getHomepage()); + existing.setDescription(imported.getDescription()); + if (imported.getPlugins() != null) { + existing.setPlugins(new ArrayList<>(imported.getPlugins())); + } + config.ensureValidPrimary(); + } + } + + private static String stringVal(Object o) { + if (o == null) { + return null; + } + String s = String.valueOf(o).trim(); + return s.isEmpty() ? null : s; + } + + private static boolean boolVal(Object o, boolean defaultValue) { + if (o == null) { + return defaultValue; + } + if (o instanceof Boolean b) { + return b; + } + return Boolean.parseBoolean(String.valueOf(o).trim()); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentApplier.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentApplier.java new file mode 100644 index 00000000000..c48640e37a9 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentApplier.java @@ -0,0 +1,307 @@ +/* + * 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.marketplace.env; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.marketplace.catalog.OptionalPluginCatalog; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.marketplace.command.MarketplaceCommand; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.install.InstallReceipt; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.apache.hop.marketplace.install.PluginUninstaller; +import org.apache.hop.marketplace.resolve.MavenCoordinates; +import org.apache.hop.marketplace.resolve.MavenRepositoryClient; + +/** Applies or validates a {@link HopEnvironmentSpec} against a Hop installation. */ +public class EnvironmentApplier { + + private final ILogChannel log; + private final Path hopHome; + private final MarketplaceConfig baseConfig; + + public EnvironmentApplier(ILogChannel log, Path hopHome, MarketplaceConfig baseConfig) { + this.log = log; + this.hopHome = hopHome; + this.baseConfig = baseConfig; + } + + public EnvironmentDrift validate(HopEnvironmentSpec env) throws HopException { + EnvironmentDrift drift = new EnvironmentDrift(); + String defaultVersion = resolveEnvVersion(env); + + for (HopEnvironmentSpec.PluginRef ref : nullSafe(env.getPlugins())) { + if (StringUtils.isBlank(ref.getArtifactId())) { + continue; + } + String groupId = + StringUtils.isNotBlank(ref.getGroupId()) ? ref.getGroupId() : baseConfig.getGroupId(); + String version = StringUtils.isNotBlank(ref.getVersion()) ? ref.getVersion() : defaultVersion; + InstallReceipt receipt = PluginInstaller.readReceipt(hopHome, ref.getArtifactId()); + boolean onDisk = isPluginOnDisk(ref.getArtifactId()); + if (!onDisk && receipt == null) { + drift.getMissingPlugins().add(groupId + ":" + ref.getArtifactId() + ":" + version); + continue; + } + if (receipt != null + && StringUtils.isNotBlank(version) + && !version.equals(receipt.getVersion())) { + drift + .getVersionMismatches() + .add(ref.getArtifactId() + " local=" + receipt.getVersion() + " required=" + version); + } + } + + for (HopEnvironmentSpec.DependencyRef dep : nullSafe(env.getDependencies())) { + if (StringUtils.isAnyBlank(dep.getGroupId(), dep.getArtifactId(), dep.getVersion())) { + continue; + } + String target = StringUtils.defaultIfBlank(dep.getTarget(), "lib/jdbc"); + Path jar = + hopHome.resolve(target).resolve(dep.getArtifactId() + "-" + dep.getVersion() + ".jar"); + if (!Files.isRegularFile(jar)) { + // also accept any jar starting with artifactId- + if (!anyJarPresent(hopHome.resolve(target), dep.getArtifactId())) { + drift + .getMissingDependencies() + .add(dep.getGroupId() + ":" + dep.getArtifactId() + ":" + dep.getVersion()); + } + } + } + + return drift; + } + + /** + * Install missing plugins/deps; optionally prune marketplace plugins not listed in the env file. + */ + public void apply(HopEnvironmentSpec env, boolean prune) throws HopException { + MarketplaceConfig config = configFromEnv(env); + String defaultVersion = resolveEnvVersion(env); + PluginInstaller installer = new PluginInstaller(log, hopHome, config); + installer.activateAllPending(); + + Set desiredArtifacts = new HashSet<>(); + for (HopEnvironmentSpec.PluginRef ref : nullSafe(env.getPlugins())) { + if (StringUtils.isBlank(ref.getArtifactId())) { + continue; + } + desiredArtifacts.add(ref.getArtifactId()); + String groupId = + StringUtils.isNotBlank(ref.getGroupId()) ? ref.getGroupId() : config.getGroupId(); + String version = StringUtils.isNotBlank(ref.getVersion()) ? ref.getVersion() : defaultVersion; + InstallReceipt receipt = PluginInstaller.readReceipt(hopHome, ref.getArtifactId()); + boolean onDisk = isPluginOnDisk(ref.getArtifactId()); + // Present on disk without a receipt (e.g. install-wave1-plugins.sh) counts as satisfied. + boolean versionMismatch = + receipt != null + && StringUtils.isNotBlank(version) + && !version.equals(receipt.getVersion()); + boolean needsInstall = (!onDisk && receipt == null) || versionMismatch; + if (needsInstall) { + MavenCoordinates coords = new MavenCoordinates(groupId, ref.getArtifactId(), version); + log.logBasic("Applying environment: installing " + coords.gav()); + installer.install(coords, true); + } else { + log.logBasic("Applying environment: " + ref.getArtifactId() + " already satisfied"); + } + } + + for (HopEnvironmentSpec.DependencyRef dep : nullSafe(env.getDependencies())) { + if (StringUtils.isAnyBlank(dep.getGroupId(), dep.getArtifactId(), dep.getVersion())) { + continue; + } + String target = StringUtils.defaultIfBlank(dep.getTarget(), "lib/jdbc"); + Path dir = hopHome.resolve(target); + Path jar = dir.resolve(dep.getArtifactId() + "-" + dep.getVersion() + ".jar"); + if (Files.isRegularFile(jar)) { + log.logBasic("Dependency already present: " + jar.getFileName()); + continue; + } + MavenCoordinates coords = + new MavenCoordinates(dep.getGroupId(), dep.getArtifactId(), dep.getVersion()); + String relativePath = + coords.groupId().replace('.', '/') + + "/" + + coords.artifactId() + + "/" + + coords.version() + + "/" + + coords.artifactId() + + "-" + + coords.version() + + ".jar"; + log.logBasic("Downloading dependency " + coords.gav() + " → " + target); + try { + Files.createDirectories(dir); + new MavenRepositoryClient(log) + .downloadArtifact(config.primaryRepository(), relativePath, coords.gav(), jar); + } catch (IOException e) { + throw new HopException("Failed to install dependency " + coords.gav(), e); + } + } + + if (prune) { + pruneExtras(desiredArtifacts); + } + } + + private void pruneExtras(Set desiredArtifacts) throws HopException { + Path receiptsDir = hopHome.resolve(PluginInstaller.RECEIPTS_DIR); + if (!Files.isDirectory(receiptsDir)) { + return; + } + PluginUninstaller uninstaller = new PluginUninstaller(log, hopHome); + try (DirectoryStream stream = Files.newDirectoryStream(receiptsDir, "*.json")) { + for (Path file : stream) { + String name = file.getFileName().toString(); + String artifactId = name.substring(0, name.length() - ".json".length()); + if (!desiredArtifacts.contains(artifactId)) { + log.logBasic("Pruning marketplace plugin not in env file: " + artifactId); + uninstaller.uninstall(artifactId); + } + } + } catch (IOException e) { + throw new HopException("Failed to scan marketplace receipts for prune", e); + } + } + + private MarketplaceConfig configFromEnv(HopEnvironmentSpec env) { + MarketplaceConfig config = new MarketplaceConfig(); + config.setEnabled(baseConfig.isEnabled()); + config.setGroupId(baseConfig.getGroupId()); + config.setDefaultVersion( + StringUtils.isNotBlank(env.getHopVersion()) + ? env.getHopVersion() + : MarketplaceCommand.resolveDefaultVersion(baseConfig)); + config.getRepositories().clear(); + MarketplaceRepository baseRepo = baseConfig.primaryRepository(); + if (env.getRepositories() != null && !env.getRepositories().isEmpty()) { + boolean first = true; + for (HopEnvironmentSpec.RepositoryRef ref : env.getRepositories()) { + if (StringUtils.isNotBlank(ref.getUrl())) { + MarketplaceRepository repo = + new MarketplaceRepository( + StringUtils.defaultIfBlank(ref.getId(), "env"), + ref.getUrl(), + StringUtils.isNotBlank(ref.getUsername()) + ? ref.getUsername() + : baseRepo.getUsername(), + StringUtils.isNotBlank(ref.getPassword()) + ? ref.getPassword() + : baseRepo.getPassword()); + repo.setPrimary(first); + repo.setEnabled(true); + config.getRepositories().add(repo); + first = false; + } + } + } + if (config.getRepositories().isEmpty()) { + config.getRepositories().addAll(baseConfig.getRepositories()); + } + if (config.getRepositories().isEmpty()) { + config.getRepositories().addAll(MarketplaceConfig.defaultRepositories()); + } + config.ensureValidPrimary(); + return config; + } + + private String resolveEnvVersion(HopEnvironmentSpec env) { + if (StringUtils.isNotBlank(env.getHopVersion())) { + return env.getHopVersion(); + } + return MarketplaceCommand.resolveDefaultVersion(baseConfig); + } + + private boolean isPluginOnDisk(String artifactId) { + for (OptionalPluginInfo info : OptionalPluginCatalog.listWave1()) { + if (artifactId.equals(info.getArtifactId())) { + return OptionalPluginCatalog.isInstalledOnDisk(hopHome, info); + } + } + // fallback: any receipt counts as installed for non-catalog plugins + try { + return PluginInstaller.readReceipt(hopHome, artifactId) != null; + } catch (HopException e) { + return false; + } + } + + private static boolean anyJarPresent(Path dir, String artifactId) throws HopException { + if (!Files.isDirectory(dir)) { + return false; + } + try (DirectoryStream stream = Files.newDirectoryStream(dir, artifactId + "-*.jar")) { + return stream.iterator().hasNext(); + } catch (IOException e) { + throw new HopException("Unable to list " + dir, e); + } + } + + private static List nullSafe(List list) { + return list == null ? List.of() : list; + } + + /** Locate hop-env.yaml/json relative to hop home / project / properties. */ + public static Path resolveEnvironmentFile(Path hopHome, String explicitPath) { + if (StringUtils.isNotBlank(explicitPath)) { + return Path.of(explicitPath).toAbsolutePath().normalize(); + } + String prop = System.getProperty("hop.env.file"); + if (StringUtils.isNotBlank(prop)) { + return Path.of(prop).toAbsolutePath().normalize(); + } + String env = System.getenv("HOP_ENV_FILE"); + if (StringUtils.isNotBlank(env)) { + return Path.of(env).toAbsolutePath().normalize(); + } + String projectHome = System.getProperty("PROJECT_HOME"); + if (StringUtils.isBlank(projectHome)) { + projectHome = System.getenv("PROJECT_HOME"); + } + if (StringUtils.isNotBlank(projectHome)) { + Path p = Path.of(projectHome); + for (String name : List.of("hop-env.yaml", "hop-env.yml", "hop-env.json")) { + Path candidate = p.resolve(name); + if (Files.isRegularFile(candidate)) { + return candidate; + } + } + } + if (hopHome != null) { + for (String name : List.of("hop-env.yaml", "hop-env.yml", "hop-env.json")) { + Path candidate = hopHome.resolve(name); + if (Files.isRegularFile(candidate)) { + return candidate; + } + } + } + return null; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentDrift.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentDrift.java new file mode 100644 index 00000000000..ba5dc875d5c --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/EnvironmentDrift.java @@ -0,0 +1,55 @@ +/* + * 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.marketplace.env; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; + +/** Result of comparing a hop-env file against the local install. */ +@Getter +public class EnvironmentDrift { + private final List missingPlugins = new ArrayList<>(); + private final List versionMismatches = new ArrayList<>(); + private final List extraMarketplacePlugins = new ArrayList<>(); + private final List missingDependencies = new ArrayList<>(); + + public boolean hasDrift() { + return !missingPlugins.isEmpty() + || !versionMismatches.isEmpty() + || !extraMarketplacePlugins.isEmpty() + || !missingDependencies.isEmpty(); + } + + public String formatReport() { + StringBuilder sb = new StringBuilder(); + for (String s : missingPlugins) { + sb.append(" MISSING plugin: ").append(s).append('\n'); + } + for (String s : versionMismatches) { + sb.append(" VERSION mismatch: ").append(s).append('\n'); + } + for (String s : extraMarketplacePlugins) { + sb.append(" EXTRA marketplace plugin (not in env file): ").append(s).append('\n'); + } + for (String s : missingDependencies) { + sb.append(" MISSING dependency jar: ").append(s).append('\n'); + } + return sb.toString(); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentLoader.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentLoader.java new file mode 100644 index 00000000000..ee6cfa22a6f --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentLoader.java @@ -0,0 +1,190 @@ +/* + * 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.marketplace.env; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +/** Loads and saves {@link HopEnvironmentSpec} as YAML or JSON. */ +public final class HopEnvironmentLoader { + + private HopEnvironmentLoader() {} + + public static HopEnvironmentSpec load(Path file) throws HopException { + if (file == null || !Files.isRegularFile(file)) { + throw new HopException("Environment file not found: " + file); + } + String name = file.getFileName().toString().toLowerCase(Locale.ROOT); + try (InputStream in = Files.newInputStream(file)) { + if (name.endsWith(".json")) { + return HopJson.newMapper().readValue(in, HopEnvironmentSpec.class); + } + // default: YAML (.yaml / .yml / anything else) + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + Object loaded = yaml.load(in); + if (loaded == null) { + return new HopEnvironmentSpec(); + } + if (!(loaded instanceof Map)) { + throw new HopException("Environment file root must be a YAML mapping: " + file); + } + return HopJson.newMapper().convertValue(loaded, HopEnvironmentSpec.class); + } catch (IOException e) { + throw new HopException("Unable to read environment file: " + file, e); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to parse environment file: " + file, e); + } + } + + /** + * Writes {@code spec} to {@code file}. Format is JSON when the path ends with {@code .json}; + * otherwise YAML. + */ + public static void save(Path file, HopEnvironmentSpec spec) throws HopException { + if (file == null) { + throw new HopException("Environment file path is required"); + } + if (spec == null) { + throw new HopException("Environment specification is required"); + } + try { + Path parent = file.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + String name = file.getFileName().toString().toLowerCase(Locale.ROOT); + if (name.endsWith(".json")) { + HopJson.newMapper().writerWithDefaultPrettyPrinter().writeValue(file.toFile(), spec); + return; + } + Map map = toYamlMap(spec); + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + options.setIndent(2); + options.setIndicatorIndent(0); + Yaml yaml = new Yaml(options); + try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) { + yaml.dump(map, writer); + } + } catch (Exception e) { + throw new HopException("Unable to write environment file: " + file, e); + } + } + + /** + * Builds a LinkedHashMap with stable field order and omits blank optional fields / empty lists. + */ + static Map toYamlMap(HopEnvironmentSpec spec) { + Map root = new LinkedHashMap<>(); + root.put("version", StringUtils.defaultIfBlank(spec.getVersion(), "1.0")); + if (StringUtils.isNotBlank(spec.getHopVersion())) { + root.put("hopVersion", spec.getHopVersion()); + } + root.put("enforceOnRun", spec.isEnforceOnRun()); + + List> repos = new ArrayList<>(); + for (HopEnvironmentSpec.RepositoryRef ref : nullSafe(spec.getRepositories())) { + if (ref == null || (StringUtils.isBlank(ref.getId()) && StringUtils.isBlank(ref.getUrl()))) { + continue; + } + Map m = new LinkedHashMap<>(); + if (StringUtils.isNotBlank(ref.getId())) { + m.put("id", ref.getId()); + } + if (StringUtils.isNotBlank(ref.getUrl())) { + m.put("url", ref.getUrl()); + } + if (StringUtils.isNotBlank(ref.getUsername())) { + m.put("username", ref.getUsername()); + } + if (StringUtils.isNotBlank(ref.getPassword())) { + m.put("password", ref.getPassword()); + } + repos.add(m); + } + if (!repos.isEmpty()) { + root.put("repositories", repos); + } + + List> plugins = new ArrayList<>(); + for (HopEnvironmentSpec.PluginRef ref : nullSafe(spec.getPlugins())) { + if (ref == null || StringUtils.isBlank(ref.getArtifactId())) { + continue; + } + Map m = new LinkedHashMap<>(); + if (StringUtils.isNotBlank(ref.getGroupId())) { + m.put("groupId", ref.getGroupId()); + } + m.put("artifactId", ref.getArtifactId()); + if (StringUtils.isNotBlank(ref.getVersion())) { + m.put("version", ref.getVersion()); + } + plugins.add(m); + } + if (!plugins.isEmpty()) { + root.put("plugins", plugins); + } + + List> deps = new ArrayList<>(); + for (HopEnvironmentSpec.DependencyRef ref : nullSafe(spec.getDependencies())) { + if (ref == null + || StringUtils.isAnyBlank(ref.getGroupId(), ref.getArtifactId(), ref.getVersion())) { + continue; + } + Map m = new LinkedHashMap<>(); + m.put("groupId", ref.getGroupId()); + m.put("artifactId", ref.getArtifactId()); + m.put("version", ref.getVersion()); + String target = StringUtils.defaultIfBlank(ref.getTarget(), "lib/jdbc"); + if (!"lib/jdbc".equals(target)) { + m.put("target", target); + } else if (StringUtils.isNotBlank(ref.getTarget())) { + // keep explicit default if user set it + m.put("target", target); + } + deps.add(m); + } + if (!deps.isEmpty()) { + root.put("dependencies", deps); + } + return root; + } + + private static List nullSafe(List list) { + return list == null ? List.of() : list; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentSpec.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentSpec.java new file mode 100644 index 00000000000..1efee5f64c7 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/HopEnvironmentSpec.java @@ -0,0 +1,84 @@ +/* + * 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.marketplace.env; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** + * Declarative Hop environment file ({@code hop-env.yaml} / {@code hop-env.json}). + * + *

+ * version: "1.0"
+ * hopVersion: "2.19.0"
+ * enforceOnRun: false
+ * repositories:
+ *   - id: central
+ *     url: Maven central
+ * plugins:
+ *   - artifactId: hop-tech-parquet
+ *     version: "2.19.0"
+ * dependencies:
+ *   - groupId: org.postgresql
+ *     artifactId: postgresql
+ *     version: "42.7.3"
+ * 
+ */ +@Getter +@Setter +public class HopEnvironmentSpec { + private String version = "1.0"; + private String hopVersion; + private boolean enforceOnRun; + private List repositories = new ArrayList<>(); + private List plugins = new ArrayList<>(); + private List dependencies = new ArrayList<>(); + + @Getter + @Setter + public static class RepositoryRef { + private String id; + private String url; + + /** Optional Basic auth username (prefer env HOP_MARKETPLACE_PASSWORD for secrets). */ + private String username; + + private String password; + } + + @Getter + @Setter + public static class PluginRef { + private String groupId; + private String artifactId; + private String version; + } + + @Getter + @Setter + public static class DependencyRef { + private String groupId; + private String artifactId; + private String version; + + /** Target directory under Hop home; default {@code lib/jdbc}. */ + private String target = "lib/jdbc"; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/MarketplaceAttributes.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/MarketplaceAttributes.java new file mode 100644 index 00000000000..faef714c38e --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/env/MarketplaceAttributes.java @@ -0,0 +1,103 @@ +/* + * 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.marketplace.env; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.IAttributes; + +/** + * Attribute group/key constants for marketplace settings stored via {@link IAttributes} (e.g. on + * lifecycle environments). Group name is {@link #GROUP}; keys are short and stable for JSON. + */ +public final class MarketplaceAttributes { + + /** IAttributes group for marketplace settings. */ + public static final String GROUP = "marketplace"; + + /** Path to hop-env.yaml / full-client-env.yaml (may contain variables). */ + public static final String KEY_ENV_FILE = "envFile"; + + /** + * When environment is enabled: {@link #ON_ENABLE_OFF}, {@link #ON_ENABLE_WARN}, {@link + * #ON_ENABLE_ENFORCE}. + */ + public static final String KEY_ON_ENABLE = "onEnable"; + + /** When true, extra marketplace plugins (receipts not in env file) count as drift. */ + public static final String KEY_STRICT = "strict"; + + /** When true, apply missing installs on enable (default false). */ + public static final String KEY_AUTO_APPLY = "autoApply"; + + public static final String ON_ENABLE_OFF = "off"; + public static final String ON_ENABLE_WARN = "warn"; + public static final String ON_ENABLE_ENFORCE = "enforce"; + + private MarketplaceAttributes() {} + + /** + * Resolve on-enable policy from attributes, falling back to purpose-based defaults when unset. + * + *
    + *
  • Production → enforce + *
  • Testing / Acceptance → warn + *
  • Development / CI / CB / other → off + *
+ */ + public static String resolveOnEnable(IAttributes attributes, String purpose) { + String explicit = attributes != null ? attributes.getAttribute(GROUP, KEY_ON_ENABLE) : null; + if (StringUtils.isNotBlank(explicit)) { + return explicit.trim().toLowerCase(); + } + return defaultOnEnableForPurpose(purpose); + } + + public static String defaultOnEnableForPurpose(String purpose) { + if (StringUtils.isBlank(purpose)) { + return ON_ENABLE_OFF; + } + String p = purpose.trim().toLowerCase(); + if (p.contains("production") || p.contains("prod")) { + return ON_ENABLE_ENFORCE; + } + if (p.contains("test") || p.contains("accept")) { + return ON_ENABLE_WARN; + } + return ON_ENABLE_OFF; + } + + public static boolean isStrict(IAttributes attributes) { + if (attributes == null) { + return false; + } + String v = attributes.getAttribute(GROUP, KEY_STRICT); + return "true".equalsIgnoreCase(v) || "Y".equalsIgnoreCase(v); + } + + public static boolean isAutoApply(IAttributes attributes) { + if (attributes == null) { + return false; + } + String v = attributes.getAttribute(GROUP, KEY_AUTO_APPLY); + return "true".equalsIgnoreCase(v) || "Y".equalsIgnoreCase(v); + } + + public static String envFile(IAttributes attributes) { + return attributes != null ? attributes.getAttribute(GROUP, KEY_ENV_FILE) : null; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/AddPluginsFromCatalogDialog.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/AddPluginsFromCatalogDialog.java new file mode 100644 index 00000000000..4eea0c40103 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/AddPluginsFromCatalogDialog.java @@ -0,0 +1,195 @@ +/* + * 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.marketplace.gui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.marketplace.catalog.OptionalPluginCatalog; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.gui.GuiResource; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +/** Multi-select optional plugins from the marketplace catalog. */ +public class AddPluginsFromCatalogDialog extends Dialog { + + private static final Class PKG = MarketplaceGuiPlugin.class; + + private final PropsUi props; + private final Set alreadyListed; + private final List selected = new ArrayList<>(); + + private Shell shell; + private TableView wTable; + private Text wFilter; + + public AddPluginsFromCatalogDialog(Shell parent, Set alreadyListedArtifactIds) { + super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + this.props = PropsUi.getInstance(); + this.alreadyListed = + alreadyListedArtifactIds == null ? Set.of() : new HashSet<>(alreadyListedArtifactIds); + } + + /** + * @return selected catalog entries, or empty if cancelled + */ + public List open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + PropsUi.setLook(shell); + shell.setImage(GuiResource.getInstance().getImageHopUi()); + shell.setText(BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Shell.Title")); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + + Label wlFilter = new Label(shell, SWT.RIGHT); + PropsUi.setLook(wlFilter); + wlFilter.setText(BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Filter.Label")); + FormData fdlFilter = new FormData(); + fdlFilter.left = new FormAttachment(0, 0); + fdlFilter.top = new FormAttachment(0, 0); + wlFilter.setLayoutData(fdlFilter); + + wFilter = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wFilter); + FormData fdFilter = new FormData(); + fdFilter.left = new FormAttachment(wlFilter, PropsUi.getMargin()); + fdFilter.top = new FormAttachment(wlFilter, 0, SWT.CENTER); + fdFilter.right = new FormAttachment(100, 0); + wFilter.setLayoutData(fdFilter); + wFilter.addListener(SWT.Modify, e -> refreshTable()); + + Button wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> shell.dispose()); + + Button wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Button.Add")); + wOk.addListener(SWT.Selection, e -> ok()); + + BaseTransformDialog.positionBottomButtons( + shell, new Button[] {wOk, wCancel}, PropsUi.getMargin(), null); + + ColumnInfo[] columns = { + new ColumnInfo( + BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Column.Artifact"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Column.Category"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "AddPluginsFromCatalogDialog.Column.Description"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }; + wTable = + new TableView( + Variables.getADefaultVariableSpace(), + shell, + SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL, + columns, + 1, + true, + null, + props, + false); + PropsUi.setLook(wTable); + + FormData fdTable = new FormData(); + fdTable.left = new FormAttachment(0, 0); + fdTable.top = new FormAttachment(wFilter, PropsUi.getMargin() * 2); + fdTable.right = new FormAttachment(100, 0); + fdTable.bottom = new FormAttachment(wOk, -PropsUi.getMargin() * 2); + wTable.setLayoutData(fdTable); + + refreshTable(); + + BaseTransformDialog.setSize(shell); + shell.setSize(Math.max(shell.getSize().x, 720), Math.max(shell.getSize().y, 480)); + shell.open(); + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + return selected; + } + + private void refreshTable() { + wTable.table.removeAll(); + String filter = wFilter != null ? wFilter.getText() : ""; + for (OptionalPluginInfo info : OptionalPluginCatalog.query(filter)) { + if (info == null || StringUtils.isBlank(info.getArtifactId())) { + continue; + } + if (alreadyListed.contains(info.getArtifactId())) { + continue; + } + TableItem item = new TableItem(wTable.table, SWT.NONE); + item.setText(1, Const.NVL(info.getName(), info.getArtifactId())); + item.setText(2, Const.NVL(info.getArtifactId(), "")); + item.setText(3, Const.NVL(info.getCategory(), "")); + item.setText(4, Const.NVL(info.getDescription(), "")); + item.setData(info); + } + wTable.optimizeTableView(); + } + + private void ok() { + selected.clear(); + for (TableItem item : wTable.table.getSelection()) { + if (item.getData() instanceof OptionalPluginInfo info) { + selected.add(info); + } + } + shell.dispose(); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/HopEnvironmentDialog.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/HopEnvironmentDialog.java new file mode 100644 index 00000000000..08c9df8c61a --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/HopEnvironmentDialog.java @@ -0,0 +1,1516 @@ +/* + * 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.marketplace.gui; + +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.history.AuditList; +import org.apache.hop.history.AuditManager; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.env.EnvironmentApplier; +import org.apache.hop.marketplace.env.EnvironmentDrift; +import org.apache.hop.marketplace.env.HopEnvironmentLoader; +import org.apache.hop.marketplace.env.HopEnvironmentSpec; +import org.apache.hop.marketplace.install.HopHome; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.gui.GuiResource; +import org.apache.hop.ui.core.gui.HopNamespace; +import org.apache.hop.ui.core.gui.WindowProperty; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.hopgui.shared.AuditManagerGuiUtil; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +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; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +/** + * Graphical editor for marketplace environment files ({@code hop-env.yaml} / {@code .json}). + * + *

Covers all {@link HopEnvironmentSpec} fields: general settings, repositories, plugins, and jar + * dependencies. + */ +@Getter +@Setter +public class HopEnvironmentDialog extends Dialog { + + private static final Class PKG = MarketplaceGuiPlugin.class; + + /** Audit list type for recently opened/saved marketplace environment files. */ + public static final String AUDIT_TYPE_ENV_FILES = "MarketplaceEnvFiles"; + + private final PropsUi props; + private final ILogChannel log = new LogChannel("MarketplaceEnvironment"); + private final Path hopHome; + private final MarketplaceConfig config; + + private Shell shell; + private Combo wFileCombo; + private Text wVersion; + private Text wHopVersion; + private Button wEnforceOnRun; + private Button wPrune; + private Button wStrict; + private TableView wRepos; + private TableView wPlugins; + private TableView wDependencies; + + private Path currentPath; + private boolean dirty; + private boolean ignoreDirtyEvents; + private boolean saved; + private Path resultPath; + private boolean embedded; + private java.util.function.Consumer statusListener; + + public HopEnvironmentDialog(Shell parent, Path initialPath) { + super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + this.props = PropsUi.getInstance(); + Path home; + MarketplaceConfig cfg; + try { + home = HopHome.resolve(); + cfg = MarketplaceConfig.load(); + } catch (Exception e) { + home = Path.of(System.getProperty("user.dir", ".")).toAbsolutePath().normalize(); + cfg = new MarketplaceConfig(); + } + this.hopHome = home; + this.config = cfg; + this.currentPath = initialPath; + } + + /** + * Embed the environment editor into an existing composite (e.g. Marketplace Environment tab). + * + * @param parent composite with FormLayout + * @param initialPath optional hop-env path to load + * @param statusListener optional status line callback (may be null) + */ + public static void embed(Composite parent, Path initialPath, Consumer statusListener) { + HopEnvironmentDialog editor = new HopEnvironmentDialog(parent.getShell(), initialPath); + editor.embedded = true; + editor.statusListener = statusListener; + editor.shell = parent.getShell(); + editor.createControls(parent, false); + if (initialPath != null && Files.isRegularFile(initialPath)) { + editor.loadFromPath(initialPath); + } else { + editor.loadSpec(newEmptySpec(), null); + } + } + + public Path open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + PropsUi.setLook(shell); + shell.setImage(GuiResource.getInstance().getImageHopUi()); + updateTitle(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + + createControls(shell, true); + + shell.addListener( + SWT.Close, + e -> { + e.doit = false; + close(); + }); + + BaseTransformDialog.setSize(shell); + shell.setSize(Math.max(shell.getSize().x, 820), Math.max(shell.getSize().y, 560)); + shell.open(); + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + return resultPath; + } + + private void createControls(Composite parent, boolean includeClose) { + // File toolbar + Button wNew = new Button(parent, SWT.PUSH); + wNew.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.New")); + wNew.addListener(SWT.Selection, e -> newFile()); + FormData fdNew = new FormData(); + fdNew.left = new FormAttachment(0, 0); + fdNew.top = new FormAttachment(0, 0); + wNew.setLayoutData(fdNew); + + Button wOpen = new Button(parent, SWT.PUSH); + wOpen.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Open")); + wOpen.addListener(SWT.Selection, e -> openFile()); + FormData fdOpen = new FormData(); + fdOpen.left = new FormAttachment(wNew, PropsUi.getMargin()); + fdOpen.top = new FormAttachment(0, 0); + wOpen.setLayoutData(fdOpen); + + Button wSave = new Button(parent, SWT.PUSH); + wSave.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Save")); + wSave.addListener(SWT.Selection, e -> save()); + FormData fdSave = new FormData(); + fdSave.left = new FormAttachment(wOpen, PropsUi.getMargin()); + fdSave.top = new FormAttachment(0, 0); + wSave.setLayoutData(fdSave); + + Button wSaveAs = new Button(parent, SWT.PUSH); + wSaveAs.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.SaveAs")); + wSaveAs.addListener(SWT.Selection, e -> saveAs()); + FormData fdSaveAs = new FormData(); + fdSaveAs.left = new FormAttachment(wSave, PropsUi.getMargin()); + fdSaveAs.top = new FormAttachment(0, 0); + wSaveAs.setLayoutData(fdSaveAs); + + wFileCombo = new Combo(parent, SWT.DROP_DOWN | SWT.BORDER); + PropsUi.setLook(wFileCombo); + FormData fdFile = new FormData(); + fdFile.left = new FormAttachment(wSaveAs, PropsUi.getMargin() * 2); + fdFile.right = new FormAttachment(100, 0); + fdFile.top = new FormAttachment(wNew, 0, SWT.CENTER); + wFileCombo.setLayoutData(fdFile); + refreshFileComboItems(currentPath != null ? currentPath.toString() : ""); + wFileCombo.addListener(SWT.Selection, e -> onFileComboSelected()); + wFileCombo.addListener(SWT.DefaultSelection, e -> onFileComboSelected()); + + Label wToolbarSep = new Label(parent, SWT.HORIZONTAL | SWT.SEPARATOR); + FormData fdToolbarSep = new FormData(); + fdToolbarSep.left = new FormAttachment(0, 0); + fdToolbarSep.right = new FormAttachment(100, 0); + fdToolbarSep.top = new FormAttachment(wNew, PropsUi.getMargin()); + wToolbarSep.setLayoutData(fdToolbarSep); + + // Bottom buttons + Button wApply = new Button(parent, SWT.PUSH); + wApply.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Apply")); + wApply.setToolTipText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Apply.Tooltip")); + wApply.addListener(SWT.Selection, e -> apply()); + + Button wValidate = new Button(parent, SWT.PUSH); + wValidate.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Validate")); + wValidate.setToolTipText( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Validate.Tooltip")); + wValidate.addListener(SWT.Selection, e -> validate()); + + Button wClose = null; + if (includeClose) { + wClose = new Button(parent, SWT.PUSH); + wClose.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Close")); + wClose.addListener(SWT.Selection, e -> close()); + BaseTransformDialog.positionBottomButtons( + parent, new Button[] {wValidate, wApply, wClose}, PropsUi.getMargin(), null); + } else { + BaseTransformDialog.positionBottomButtons( + parent, new Button[] {wValidate, wApply}, PropsUi.getMargin(), null); + } + + wPrune = new Button(parent, SWT.CHECK); + PropsUi.setLook(wPrune); + wPrune.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Prune")); + wPrune.setToolTipText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Prune.Tooltip")); + FormData fdPrune = new FormData(); + fdPrune.left = new FormAttachment(0, 0); + fdPrune.bottom = new FormAttachment(wValidate, -PropsUi.getMargin()); + wPrune.setLayoutData(fdPrune); + + wStrict = new Button(parent, SWT.CHECK); + PropsUi.setLook(wStrict); + wStrict.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Strict")); + wStrict.setToolTipText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Strict.Tooltip")); + FormData fdStrict = new FormData(); + fdStrict.left = new FormAttachment(wPrune, PropsUi.getMargin() * 3); + fdStrict.bottom = new FormAttachment(wValidate, -PropsUi.getMargin()); + wStrict.setLayoutData(fdStrict); + + CTabFolder wTabFolder = new CTabFolder(parent, SWT.BORDER); + PropsUi.setLook(wTabFolder); + FormData fdTabs = new FormData(); + fdTabs.left = new FormAttachment(0, 0); + fdTabs.top = new FormAttachment(wToolbarSep, PropsUi.getMargin()); + fdTabs.right = new FormAttachment(100, 0); + fdTabs.bottom = new FormAttachment(wPrune, -PropsUi.getMargin()); + wTabFolder.setLayoutData(fdTabs); + + createGeneralTab(wTabFolder); + createRepositoriesTab(wTabFolder); + createPluginsTab(wTabFolder); + createDependenciesTab(wTabFolder); + wTabFolder.setSelection(0); + + // Load initial content (dialog mode only; embed() loads after createControls) + if (!embedded) { + if (currentPath != null && Files.isRegularFile(currentPath)) { + loadFromPath(currentPath); + } else { + loadSpec(newEmptySpec(), null); + } + } + } + + private void createGeneralTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Tab.General")); + 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); + + Label wlHelp = new Label(comp, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.General.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + int middle = 30; + Label wlVersion = new Label(comp, SWT.RIGHT); + PropsUi.setLook(wlVersion); + wlVersion.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.General.Version")); + FormData fdlVersion = new FormData(); + fdlVersion.left = new FormAttachment(0, 0); + fdlVersion.top = new FormAttachment(wlHelp, PropsUi.getMargin() * 2); + fdlVersion.right = new FormAttachment(middle, -PropsUi.getMargin()); + wlVersion.setLayoutData(fdlVersion); + wVersion = new Text(comp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wVersion); + FormData fdVersion = new FormData(); + fdVersion.left = new FormAttachment(middle, 0); + fdVersion.top = new FormAttachment(wlVersion, 0, SWT.CENTER); + fdVersion.right = new FormAttachment(100, 0); + wVersion.setLayoutData(fdVersion); + wVersion.addListener(SWT.Modify, e -> markDirty()); + + Label wlHopVersion = new Label(comp, SWT.RIGHT); + PropsUi.setLook(wlHopVersion); + wlHopVersion.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.General.HopVersion")); + FormData fdlHopVersion = new FormData(); + fdlHopVersion.left = new FormAttachment(0, 0); + fdlHopVersion.top = new FormAttachment(wVersion, PropsUi.getMargin()); + fdlHopVersion.right = new FormAttachment(middle, -PropsUi.getMargin()); + wlHopVersion.setLayoutData(fdlHopVersion); + wHopVersion = new Text(comp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wHopVersion); + FormData fdHopVersion = new FormData(); + fdHopVersion.left = new FormAttachment(middle, 0); + fdHopVersion.top = new FormAttachment(wlHopVersion, 0, SWT.CENTER); + fdHopVersion.right = new FormAttachment(100, 0); + wHopVersion.setLayoutData(fdHopVersion); + wHopVersion.addListener(SWT.Modify, e -> markDirty()); + + wEnforceOnRun = new Button(comp, SWT.CHECK); + PropsUi.setLook(wEnforceOnRun); + wEnforceOnRun.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.General.EnforceOnRun")); + wEnforceOnRun.setToolTipText( + BaseMessages.getString(PKG, "HopEnvironmentDialog.General.EnforceOnRun.Tooltip")); + FormData fdEnforce = new FormData(); + fdEnforce.left = new FormAttachment(middle, 0); + fdEnforce.top = new FormAttachment(wHopVersion, PropsUi.getMargin() * 2); + wEnforceOnRun.setLayoutData(fdEnforce); + wEnforceOnRun.addListener(SWT.Selection, e -> markDirty()); + } + + private void createRepositoriesTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Tab.Repositories")); + tab.setImage(GuiResource.getInstance().getImageServer()); + 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); + + Label wlHelp = new Label(comp, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Repositories.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + Button wAdd = sideButton(comp, "HopEnvironmentDialog.Button.Add", wlHelp); + Button wEdit = sideButton(comp, "HopEnvironmentDialog.Button.Edit", wAdd); + Button wRemove = sideButton(comp, "HopEnvironmentDialog.Button.Remove", wEdit); + Button wUp = sideButton(comp, "HopEnvironmentDialog.Button.MoveUp", wRemove); + Button wDown = sideButton(comp, "HopEnvironmentDialog.Button.MoveDown", wUp); + Button wImport = sideButton(comp, "HopEnvironmentDialog.Button.ImportRepos", wDown); + + wAdd.addListener(SWT.Selection, e -> addRepository()); + wEdit.addListener(SWT.Selection, e -> editRepository()); + wRemove.addListener(SWT.Selection, e -> removeSelected(wRepos)); + wUp.addListener(SWT.Selection, e -> moveSelected(wRepos, -1)); + wDown.addListener(SWT.Selection, e -> moveSelected(wRepos, 1)); + wImport.addListener(SWT.Selection, e -> importReposFromConfig()); + + wRepos = + createTableView( + comp, + wlHelp, + wAdd, + new ColumnInfo[] { + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Repo.Column.Id"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Repo.Column.Url"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Repo.Column.Username"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Repo.Column.Auth"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }); + wRepos.table.addListener(SWT.DefaultSelection, e -> editRepository()); + } + + private void createPluginsTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Tab.Plugins")); + tab.setImage(GuiResource.getInstance().getImagePlugin()); + 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); + + Label wlHelp = new Label(comp, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Plugins.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + Button wCatalog = sideButton(comp, "HopEnvironmentDialog.Button.AddFromCatalog", wlHelp, null); + Button wAdd = sideButton(comp, "HopEnvironmentDialog.Button.Add", wCatalog, wCatalog); + Button wEdit = sideButton(comp, "HopEnvironmentDialog.Button.Edit", wAdd, wCatalog); + Button wRemove = sideButton(comp, "HopEnvironmentDialog.Button.Remove", wEdit, wCatalog); + Button wUp = sideButton(comp, "HopEnvironmentDialog.Button.MoveUp", wRemove, wCatalog); + Button wDown = sideButton(comp, "HopEnvironmentDialog.Button.MoveDown", wUp, wCatalog); + + wCatalog.addListener(SWT.Selection, e -> addPluginsFromCatalog()); + wAdd.addListener(SWT.Selection, e -> addPlugin()); + wEdit.addListener(SWT.Selection, e -> editPlugin()); + wRemove.addListener(SWT.Selection, e -> removeSelected(wPlugins)); + wUp.addListener(SWT.Selection, e -> moveSelected(wPlugins, -1)); + wDown.addListener(SWT.Selection, e -> moveSelected(wPlugins, 1)); + + wPlugins = + createTableView( + comp, + wlHelp, + wCatalog, + new ColumnInfo[] { + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Plugin.Column.GroupId"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Plugin.Column.ArtifactId"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Plugin.Column.Version"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }); + wPlugins.table.addListener(SWT.DefaultSelection, e -> editPlugin()); + } + + private void createDependenciesTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Tab.Dependencies")); + tab.setImage(GuiResource.getInstance().getImageLabel()); + 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); + + Label wlHelp = new Label(comp, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Dependencies.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + Button wAdd = sideButton(comp, "HopEnvironmentDialog.Button.Add", wlHelp); + Button wEdit = sideButton(comp, "HopEnvironmentDialog.Button.Edit", wAdd); + Button wRemove = sideButton(comp, "HopEnvironmentDialog.Button.Remove", wEdit); + Button wUp = sideButton(comp, "HopEnvironmentDialog.Button.MoveUp", wRemove); + Button wDown = sideButton(comp, "HopEnvironmentDialog.Button.MoveDown", wUp); + + wAdd.addListener(SWT.Selection, e -> addDependency()); + wEdit.addListener(SWT.Selection, e -> editDependency()); + wRemove.addListener(SWT.Selection, e -> removeSelected(wDependencies)); + wUp.addListener(SWT.Selection, e -> moveSelected(wDependencies, -1)); + wDown.addListener(SWT.Selection, e -> moveSelected(wDependencies, 1)); + + wDependencies = + createTableView( + comp, + wlHelp, + wAdd, + new ColumnInfo[] { + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Dep.Column.GroupId"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Dep.Column.ArtifactId"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Dep.Column.Version"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Dep.Column.Target"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }); + wDependencies.table.addListener(SWT.DefaultSelection, e -> editDependency()); + } + + private Button sideButton(Composite parent, String key, Control above) { + return sideButton(parent, key, above, null); + } + + private Button sideButton(Composite parent, String key, Control above, Control left) { + Button b = new Button(parent, SWT.PUSH); + b.setText(BaseMessages.getString(PKG, key)); + FormData fd = new FormData(); + fd.right = new FormAttachment(100, 0); + fd.top = new FormAttachment(above, PropsUi.getMargin()); + if (left == null) { + fd.left = new FormAttachment(100, (int) (-100 * PropsUi.getNativeZoomFactor())); + } else { + fd.left = new FormAttachment(left, 0, SWT.LEFT); + } + b.setLayoutData(fd); + return b; + } + + private TableView createTableView( + Composite parent, Label help, Button sideTop, ColumnInfo[] columns) { + TableView view = + new TableView( + Variables.getADefaultVariableSpace(), + parent, + SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE | SWT.V_SCROLL, + columns, + 1, + true, + null, + props, + false); + PropsUi.setLook(view); + FormData fd = new FormData(); + fd.left = new FormAttachment(0, 0); + fd.top = new FormAttachment(help, PropsUi.getMargin()); + fd.right = new FormAttachment(sideTop, -PropsUi.getMargin()); + fd.bottom = new FormAttachment(100, 0); + view.setLayoutData(fd); + return view; + } + + // --- load / save / dirty --- + + private static HopEnvironmentSpec newEmptySpec() { + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setVersion("1.0"); + return spec; + } + + private void loadFromPath(Path path) { + try { + HopEnvironmentSpec spec = HopEnvironmentLoader.load(path); + loadSpec(spec, path); + rememberEnvFile(path); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header"), + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Load", path.toString()), + e); + } + } + + private void loadSpec(HopEnvironmentSpec spec, Path path) { + ignoreDirtyEvents = true; + try { + currentPath = path; + wVersion.setText(Const.NVL(spec.getVersion(), "1.0")); + wHopVersion.setText(Const.NVL(spec.getHopVersion(), "")); + wEnforceOnRun.setSelection(spec.isEnforceOnRun()); + fillRepos(spec.getRepositories()); + fillPlugins(spec.getPlugins()); + fillDependencies(spec.getDependencies()); + wRepos.optimizeTableView(); + wPlugins.optimizeTableView(); + wDependencies.optimizeTableView(); + dirty = false; + updateFileLabel(); + updateTitle(); + } finally { + ignoreDirtyEvents = false; + } + } + + private void fillRepos(List list) { + wRepos.table.removeAll(); + if (list == null) { + return; + } + for (HopEnvironmentSpec.RepositoryRef ref : list) { + if (ref == null) { + continue; + } + addRepoItem(ref); + } + } + + private void addRepoItem(HopEnvironmentSpec.RepositoryRef ref) { + TableItem item = new TableItem(wRepos.table, SWT.NONE); + item.setText(1, Const.NVL(ref.getId(), "")); + item.setText(2, Const.NVL(ref.getUrl(), "")); + item.setText(3, Const.NVL(ref.getUsername(), "")); + item.setText(4, StringUtils.isNotBlank(ref.getPassword()) ? "Y" : ""); + item.setData(ref); + } + + private void fillPlugins(List list) { + wPlugins.table.removeAll(); + if (list == null) { + return; + } + for (HopEnvironmentSpec.PluginRef ref : list) { + if (ref == null) { + continue; + } + addPluginItem(ref); + } + } + + private void addPluginItem(HopEnvironmentSpec.PluginRef ref) { + TableItem item = new TableItem(wPlugins.table, SWT.NONE); + item.setText(1, Const.NVL(ref.getGroupId(), "")); + item.setText(2, Const.NVL(ref.getArtifactId(), "")); + item.setText(3, Const.NVL(ref.getVersion(), "")); + item.setData(ref); + } + + private void fillDependencies(List list) { + wDependencies.table.removeAll(); + if (list == null) { + return; + } + for (HopEnvironmentSpec.DependencyRef ref : list) { + if (ref == null) { + continue; + } + addDepItem(ref); + } + } + + private void addDepItem(HopEnvironmentSpec.DependencyRef ref) { + TableItem item = new TableItem(wDependencies.table, SWT.NONE); + item.setText(1, Const.NVL(ref.getGroupId(), "")); + item.setText(2, Const.NVL(ref.getArtifactId(), "")); + item.setText(3, Const.NVL(ref.getVersion(), "")); + item.setText(4, Const.NVL(ref.getTarget(), "lib/jdbc")); + item.setData(ref); + } + + private HopEnvironmentSpec collectSpec() { + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setVersion(StringUtils.defaultIfBlank(wVersion.getText(), "1.0").trim()); + String hopVer = wHopVersion.getText(); + spec.setHopVersion(StringUtils.isBlank(hopVer) ? null : hopVer.trim()); + spec.setEnforceOnRun(wEnforceOnRun.getSelection()); + + List repos = new ArrayList<>(); + for (TableItem item : wRepos.table.getItems()) { + if (item.getData() instanceof HopEnvironmentSpec.RepositoryRef ref) { + repos.add(ref); + } + } + spec.setRepositories(repos); + + List plugins = new ArrayList<>(); + for (TableItem item : wPlugins.table.getItems()) { + if (item.getData() instanceof HopEnvironmentSpec.PluginRef ref) { + plugins.add(ref); + } + } + spec.setPlugins(plugins); + + List deps = new ArrayList<>(); + for (TableItem item : wDependencies.table.getItems()) { + if (item.getData() instanceof HopEnvironmentSpec.DependencyRef ref) { + deps.add(ref); + } + } + spec.setDependencies(deps); + return spec; + } + + private void markDirty() { + if (ignoreDirtyEvents) { + return; + } + if (!dirty) { + dirty = true; + updateTitle(); + } + } + + private void updateTitle() { + if (embedded) { + return; + } + String name = + currentPath != null + ? currentPath.getFileName().toString() + : BaseMessages.getString(PKG, "HopEnvironmentDialog.Untitled"); + String title = + BaseMessages.getString(PKG, "HopEnvironmentDialog.Shell.Title", name) + (dirty ? " *" : ""); + if (shell != null && !shell.isDisposed()) { + shell.setText(title); + } + } + + private void updateFileLabel() { + if (wFileCombo == null || wFileCombo.isDisposed()) { + return; + } + if (currentPath != null) { + refreshFileComboItems(currentPath.toString()); + } else { + refreshFileComboItems(""); + wFileCombo.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Untitled")); + } + } + + /** + * Load recent filenames from {@link AuditManager#getActive()}{@code .retrieveList(...)} into the + * combo and select {@code selectedPath} when non-blank. + */ + private void refreshFileComboItems(String selectedPath) { + if (wFileCombo == null || wFileCombo.isDisposed()) { + return; + } + try { + AuditList list = + AuditManager.getActive().retrieveList(HopNamespace.getNamespace(), AUDIT_TYPE_ENV_FILES); + if (list != null && list.getNames() != null && !list.getNames().isEmpty()) { + wFileCombo.setItems(list.getNames().toArray(new String[0])); + } else { + wFileCombo.setItems(new String[0]); + } + } catch (Exception e) { + log.logError("Unable to load marketplace environment file audit list", e); + wFileCombo.setItems(AuditManagerGuiUtil.getLastUsedValues(AUDIT_TYPE_ENV_FILES)); + } + if (StringUtils.isNotBlank(selectedPath)) { + wFileCombo.setText(selectedPath); + } + } + + /** Remember path in the audit list (most-recent first) and refresh the combo. */ + private void rememberEnvFile(Path path) { + if (path == null) { + return; + } + String absolute = path.toAbsolutePath().normalize().toString(); + AuditManagerGuiUtil.addLastUsedValue(AUDIT_TYPE_ENV_FILES, absolute); + refreshFileComboItems(absolute); + } + + private void onFileComboSelected() { + if (wFileCombo == null || wFileCombo.isDisposed()) { + return; + } + String text = wFileCombo.getText(); + if (StringUtils.isBlank(text)) { + return; + } + Path p = Path.of(text.trim()).toAbsolutePath().normalize(); + if (currentPath != null && currentPath.equals(p)) { + return; + } + if (!Files.isRegularFile(p)) { + return; + } + if (confirmDiscardIfDirty()) { + updateFileLabel(); + return; + } + loadFromPath(p); + } + + private boolean confirmDiscardIfDirty() { + if (!dirty) { + return false; + } + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.CANCEL | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Dirty.Header")); + box.setMessage(BaseMessages.getString(PKG, "HopEnvironmentDialog.Dirty.Message")); + int answer = box.open(); + if (answer == SWT.CANCEL) { + return true; + } + if (answer == SWT.YES) { + return !save(); + } + return false; + } + + private void newFile() { + if (confirmDiscardIfDirty()) { + return; + } + loadSpec(newEmptySpec(), null); + } + + private void openFile() { + if (confirmDiscardIfDirty()) { + return; + } + String path = + BaseDialog.presentFileDialog( + false, + shell, + new String[] {"*.yaml;*.yml;*.json", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.Env"), + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.All") + }, + false); + if (StringUtils.isBlank(path)) { + return; + } + Path p = Path.of(path.trim()).toAbsolutePath().normalize(); + if (!Files.isRegularFile(p)) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.NotFound.Header")); + box.setMessage( + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.NotFound.Message", p.toString())); + box.open(); + return; + } + loadFromPath(p); + rememberEnvFile(p); + } + + private boolean save() { + if (currentPath == null) { + return saveAs(); + } + return writeTo(currentPath); + } + + private boolean saveAs() { + String path = + BaseDialog.presentFileDialog( + true, + shell, + new String[] {"*.yaml;*.yml", "*.json", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "HopEnvironmentDialog.Filter.Yaml"), + BaseMessages.getString(PKG, "HopEnvironmentDialog.Filter.Json"), + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.All") + }, + false); + if (StringUtils.isBlank(path)) { + return false; + } + Path p = Path.of(path.trim()).toAbsolutePath().normalize(); + String name = p.getFileName().toString().toLowerCase(); + if (!name.endsWith(".yaml") && !name.endsWith(".yml") && !name.endsWith(".json")) { + p = p.resolveSibling(p.getFileName().toString() + ".yaml"); + } + return writeTo(p); + } + + private boolean writeTo(Path path) { + try { + HopEnvironmentSpec spec = collectSpec(); + String validationError = validateSpec(spec); + if (validationError != null) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.Header")); + box.setMessage(validationError); + box.open(); + return false; + } + HopEnvironmentLoader.save(path, spec); + currentPath = path; + resultPath = path; + saved = true; + dirty = false; + rememberEnvFile(path); + updateFileLabel(); + updateTitle(); + return true; + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header"), + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Save", path.toString()), + e); + return false; + } + } + + private String validateSpec(HopEnvironmentSpec spec) { + for (HopEnvironmentSpec.RepositoryRef ref : spec.getRepositories()) { + if (StringUtils.isBlank(ref.getId()) || StringUtils.isBlank(ref.getUrl())) { + return BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.RepoIdUrl"); + } + } + for (HopEnvironmentSpec.PluginRef ref : spec.getPlugins()) { + if (StringUtils.isBlank(ref.getArtifactId())) { + return BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.PluginArtifact"); + } + } + for (HopEnvironmentSpec.DependencyRef ref : spec.getDependencies()) { + if (StringUtils.isAnyBlank(ref.getGroupId(), ref.getArtifactId(), ref.getVersion())) { + return BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.DepGav"); + } + } + return null; + } + + private boolean ensureSavedForAction() { + if (dirty || currentPath == null) { + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.SaveFirst.Header")); + box.setMessage(BaseMessages.getString(PKG, "HopEnvironmentDialog.SaveFirst.Message")); + if (box.open() != SWT.YES) { + return true; + } + if (!save()) { + return true; + } + } + return currentPath == null || !Files.isRegularFile(currentPath); + } + + private void validate() { + if (ensureSavedForAction()) { + return; + } + try { + HopEnvironmentSpec env = HopEnvironmentLoader.load(currentPath); + EnvironmentApplier applier = new EnvironmentApplier(log, hopHome, MarketplaceConfig.load()); + EnvironmentDrift drift = applier.validate(env); + if (wStrict.getSelection()) { + populateExtraPlugins(env, drift); + } + boolean hard = + !drift.getMissingPlugins().isEmpty() + || !drift.getVersionMismatches().isEmpty() + || !drift.getMissingDependencies().isEmpty() + || (wStrict.getSelection() && !drift.getExtraMarketplacePlugins().isEmpty()); + if (!hard) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Validate.Ok.Header")); + box.setMessage( + BaseMessages.getString( + PKG, "MarketplaceDialog.Validate.Ok.Message", currentPath.toString())); + box.open(); + notifyStatus(BaseMessages.getString(PKG, "MarketplaceDialog.Status.RestartHint")); + return; + } + notifyStatus(BaseMessages.getString(PKG, "MarketplaceDialog.Status.Drift")); + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Validate.Drift.Header")); + box.setMessage( + BaseMessages.getString(PKG, "MarketplaceDialog.Validate.Drift.Message") + + "\n\n" + + drift.formatReport() + + "\n" + + BaseMessages.getString(PKG, "MarketplaceDialog.Validate.Drift.Hint")); + box.open(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.Validate.Error"), + e); + } + } + + private void apply() { + if (ensureSavedForAction()) { + return; + } + boolean prune = wPrune.getSelection(); + if (prune) { + MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + confirm.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Apply.PruneConfirm.Header")); + confirm.setMessage( + BaseMessages.getString(PKG, "MarketplaceDialog.Apply.PruneConfirm.Message")); + if (confirm.open() != SWT.YES) { + return; + } + } + try { + MarketplaceConfig live = MarketplaceConfig.load(); + if (!live.isEnabled()) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header")); + box.setMessage(BaseMessages.getString(PKG, "MarketplaceDialog.Apply.Disabled")); + box.open(); + return; + } + HopEnvironmentSpec env = HopEnvironmentLoader.load(currentPath); + new EnvironmentApplier(log, hopHome, live).apply(env, prune); + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Apply.Done.Header")); + box.setMessage( + BaseMessages.getString( + PKG, "MarketplaceDialog.Apply.Done.Message", currentPath.toString())); + box.open(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.Apply.Error"), + e); + } + } + + private void populateExtraPlugins(HopEnvironmentSpec env, EnvironmentDrift drift) + throws Exception { + Set desired = new HashSet<>(); + if (env.getPlugins() != null) { + for (HopEnvironmentSpec.PluginRef ref : env.getPlugins()) { + if (ref.getArtifactId() != null) { + desired.add(ref.getArtifactId()); + } + } + } + Path receipts = hopHome.resolve(PluginInstaller.RECEIPTS_DIR); + if (!Files.isDirectory(receipts)) { + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(receipts, "*.json")) { + for (Path f : stream) { + String name = f.getFileName().toString(); + String id = name.substring(0, name.length() - ".json".length()); + if (!desired.contains(id)) { + drift.getExtraMarketplacePlugins().add(id); + } + } + } + } + + private void close() { + if (embedded) { + return; + } + if (confirmDiscardIfDirty()) { + return; + } + props.setScreen(new WindowProperty(shell)); + shell.dispose(); + } + + // --- repository CRUD --- + + private void addRepository() { + HopEnvironmentSpec.RepositoryRef ref = new HopEnvironmentSpec.RepositoryRef(); + if (editRepositoryDialog(ref, true)) { + addRepoItem(ref); + markDirty(); + } + } + + private void editRepository() { + TableItem[] sel = wRepos.table.getSelection(); + if (sel == null || sel.length == 0) { + return; + } + HopEnvironmentSpec.RepositoryRef ref = (HopEnvironmentSpec.RepositoryRef) sel[0].getData(); + if (ref == null) { + return; + } + if (editRepositoryDialog(ref, false)) { + sel[0].setText(1, Const.NVL(ref.getId(), "")); + sel[0].setText(2, Const.NVL(ref.getUrl(), "")); + sel[0].setText(3, Const.NVL(ref.getUsername(), "")); + sel[0].setText(4, StringUtils.isNotBlank(ref.getPassword()) ? "Y" : ""); + markDirty(); + } + } + + private boolean editRepositoryDialog(HopEnvironmentSpec.RepositoryRef ref, boolean isNew) { + Shell dialog = new Shell(shell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE); + PropsUi.setLook(dialog); + dialog.setText( + BaseMessages.getString( + PKG, + isNew ? "HopEnvironmentDialog.Repo.Edit.Add" : "HopEnvironmentDialog.Repo.Edit.Edit")); + FormLayout layout = new FormLayout(); + layout.marginWidth = PropsUi.getFormMargin(); + layout.marginHeight = PropsUi.getFormMargin(); + dialog.setLayout(layout); + int middle = 30; + int margin = PropsUi.getMargin(); + + Label wlId = labeled(dialog, "HopEnvironmentDialog.Repo.Column.Id", 0, middle, margin); + Text wId = textField(dialog, wlId, middle, Const.NVL(ref.getId(), "")); + Label wlUrl = labeled(dialog, "HopEnvironmentDialog.Repo.Column.Url", wId, middle, margin); + Text wUrl = textField(dialog, wlUrl, middle, Const.NVL(ref.getUrl(), "")); + Label wlUser = + labeled(dialog, "HopEnvironmentDialog.Repo.Column.Username", wUrl, middle, margin); + Text wUser = textField(dialog, wlUser, middle, Const.NVL(ref.getUsername(), "")); + Label wlPass = + labeled(dialog, "HopEnvironmentDialog.Repo.Column.Password", wUser, middle, margin); + Text wPass = new Text(dialog, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.PASSWORD); + PropsUi.setLook(wPass); + wPass.setText(Const.NVL(ref.getPassword(), "")); + FormData fdPass = new FormData(); + fdPass.left = new FormAttachment(middle, 0); + fdPass.top = new FormAttachment(wlPass, 0, SWT.CENTER); + fdPass.right = new FormAttachment(100, 0); + wPass.setLayoutData(fdPass); + + final boolean[] ok = {false}; + Button wOk = new Button(dialog, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Ok")); + Button wCancel = new Button(dialog, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> dialog.dispose()); + wOk.addListener( + SWT.Selection, + e -> { + if (StringUtils.isBlank(wId.getText()) || StringUtils.isBlank(wUrl.getText())) { + MessageBox box = new MessageBox(dialog, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.Header")); + box.setMessage( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.RepoIdUrl")); + box.open(); + return; + } + ref.setId(wId.getText().trim()); + ref.setUrl(wUrl.getText().trim()); + ref.setUsername(blankToNull(wUser.getText())); + ref.setPassword(blankToNull(wPass.getText())); + ok[0] = true; + dialog.dispose(); + }); + BaseTransformDialog.positionBottomButtons(dialog, new Button[] {wOk, wCancel}, margin, wPass); + BaseTransformDialog.setSize(dialog); + dialog.open(); + Display display = shell.getDisplay(); + while (!dialog.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + return ok[0]; + } + + private void importReposFromConfig() { + try { + MarketplaceConfig live = config != null ? config : MarketplaceConfig.load(); + if (live.getRepositories() == null || live.getRepositories().isEmpty()) { + return; + } + MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + confirm.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.ImportRepos.Header")); + confirm.setMessage(BaseMessages.getString(PKG, "HopEnvironmentDialog.ImportRepos.Message")); + if (confirm.open() != SWT.YES) { + return; + } + wRepos.table.removeAll(); + for (MarketplaceRepository repo : live.getRepositories()) { + if (repo == null || !repo.isEnabled()) { + continue; + } + HopEnvironmentSpec.RepositoryRef ref = new HopEnvironmentSpec.RepositoryRef(); + ref.setId(repo.getId()); + ref.setUrl(repo.getUrl()); + ref.setUsername(repo.getUsername()); + ref.setPassword(repo.getPassword()); + addRepoItem(ref); + } + markDirty(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.Header"), + BaseMessages.getString(PKG, "HopEnvironmentDialog.Error.ImportRepos"), + e); + } + } + + // --- plugin CRUD --- + + private void addPlugin() { + HopEnvironmentSpec.PluginRef ref = new HopEnvironmentSpec.PluginRef(); + ref.setVersion(StringUtils.trimToNull(wHopVersion.getText())); + if (editPluginDialog(ref, true)) { + addPluginItem(ref); + markDirty(); + } + } + + private void editPlugin() { + TableItem[] sel = wPlugins.table.getSelection(); + if (sel == null || sel.length == 0) { + return; + } + HopEnvironmentSpec.PluginRef ref = (HopEnvironmentSpec.PluginRef) sel[0].getData(); + if (ref == null) { + return; + } + if (editPluginDialog(ref, false)) { + sel[0].setText(1, Const.NVL(ref.getGroupId(), "")); + sel[0].setText(2, Const.NVL(ref.getArtifactId(), "")); + sel[0].setText(3, Const.NVL(ref.getVersion(), "")); + markDirty(); + } + } + + private boolean editPluginDialog(HopEnvironmentSpec.PluginRef ref, boolean isNew) { + Shell dialog = new Shell(shell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE); + PropsUi.setLook(dialog); + dialog.setText( + BaseMessages.getString( + PKG, + isNew + ? "HopEnvironmentDialog.Plugin.Edit.Add" + : "HopEnvironmentDialog.Plugin.Edit.Edit")); + FormLayout layout = new FormLayout(); + layout.marginWidth = PropsUi.getFormMargin(); + layout.marginHeight = PropsUi.getFormMargin(); + dialog.setLayout(layout); + int middle = 30; + int margin = PropsUi.getMargin(); + + Label wlGroup = + labeled(dialog, "HopEnvironmentDialog.Plugin.Column.GroupId", 0, middle, margin); + Text wGroup = textField(dialog, wlGroup, middle, Const.NVL(ref.getGroupId(), "")); + Label wlArt = + labeled(dialog, "HopEnvironmentDialog.Plugin.Column.ArtifactId", wGroup, middle, margin); + Text wArt = textField(dialog, wlArt, middle, Const.NVL(ref.getArtifactId(), "")); + Label wlVer = + labeled(dialog, "HopEnvironmentDialog.Plugin.Column.Version", wArt, middle, margin); + Text wVer = textField(dialog, wlVer, middle, Const.NVL(ref.getVersion(), "")); + + final boolean[] ok = {false}; + Button wOk = new Button(dialog, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Ok")); + Button wCancel = new Button(dialog, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> dialog.dispose()); + wOk.addListener( + SWT.Selection, + e -> { + if (StringUtils.isBlank(wArt.getText())) { + MessageBox box = new MessageBox(dialog, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.Header")); + box.setMessage( + BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.PluginArtifact")); + box.open(); + return; + } + ref.setGroupId(blankToNull(wGroup.getText())); + ref.setArtifactId(wArt.getText().trim()); + ref.setVersion(blankToNull(wVer.getText())); + ok[0] = true; + dialog.dispose(); + }); + BaseTransformDialog.positionBottomButtons(dialog, new Button[] {wOk, wCancel}, margin, wVer); + BaseTransformDialog.setSize(dialog); + dialog.open(); + Display display = shell.getDisplay(); + while (!dialog.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + return ok[0]; + } + + private void addPluginsFromCatalog() { + Set existing = new HashSet<>(); + for (TableItem item : wPlugins.table.getItems()) { + if (item.getData() instanceof HopEnvironmentSpec.PluginRef ref + && StringUtils.isNotBlank(ref.getArtifactId())) { + existing.add(ref.getArtifactId()); + } + } + List chosen = new AddPluginsFromCatalogDialog(shell, existing).open(); + if (chosen == null || chosen.isEmpty()) { + return; + } + String defaultVersion = StringUtils.trimToNull(wHopVersion.getText()); + for (OptionalPluginInfo info : chosen) { + HopEnvironmentSpec.PluginRef ref = new HopEnvironmentSpec.PluginRef(); + ref.setArtifactId(info.getArtifactId()); + ref.setVersion(defaultVersion); + addPluginItem(ref); + } + markDirty(); + } + + // --- dependency CRUD --- + + private void addDependency() { + HopEnvironmentSpec.DependencyRef ref = new HopEnvironmentSpec.DependencyRef(); + ref.setTarget("lib/jdbc"); + if (editDependencyDialog(ref, true)) { + addDepItem(ref); + markDirty(); + } + } + + private void editDependency() { + TableItem[] sel = wDependencies.table.getSelection(); + if (sel == null || sel.length == 0) { + return; + } + HopEnvironmentSpec.DependencyRef ref = (HopEnvironmentSpec.DependencyRef) sel[0].getData(); + if (ref == null) { + return; + } + if (editDependencyDialog(ref, false)) { + sel[0].setText(1, Const.NVL(ref.getGroupId(), "")); + sel[0].setText(2, Const.NVL(ref.getArtifactId(), "")); + sel[0].setText(3, Const.NVL(ref.getVersion(), "")); + sel[0].setText(4, Const.NVL(ref.getTarget(), "lib/jdbc")); + markDirty(); + } + } + + private boolean editDependencyDialog(HopEnvironmentSpec.DependencyRef ref, boolean isNew) { + Shell dialog = new Shell(shell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE); + PropsUi.setLook(dialog); + dialog.setText( + BaseMessages.getString( + PKG, + isNew ? "HopEnvironmentDialog.Dep.Edit.Add" : "HopEnvironmentDialog.Dep.Edit.Edit")); + FormLayout layout = new FormLayout(); + layout.marginWidth = PropsUi.getFormMargin(); + layout.marginHeight = PropsUi.getFormMargin(); + dialog.setLayout(layout); + int middle = 30; + int margin = PropsUi.getMargin(); + + Label wlGroup = labeled(dialog, "HopEnvironmentDialog.Dep.Column.GroupId", 0, middle, margin); + Text wGroup = textField(dialog, wlGroup, middle, Const.NVL(ref.getGroupId(), "")); + Label wlArt = + labeled(dialog, "HopEnvironmentDialog.Dep.Column.ArtifactId", wGroup, middle, margin); + Text wArt = textField(dialog, wlArt, middle, Const.NVL(ref.getArtifactId(), "")); + Label wlVer = labeled(dialog, "HopEnvironmentDialog.Dep.Column.Version", wArt, middle, margin); + Text wVer = textField(dialog, wlVer, middle, Const.NVL(ref.getVersion(), "")); + Label wlTarget = + labeled(dialog, "HopEnvironmentDialog.Dep.Column.Target", wVer, middle, margin); + Text wTarget = textField(dialog, wlTarget, middle, Const.NVL(ref.getTarget(), "lib/jdbc")); + + final boolean[] ok = {false}; + Button wOk = new Button(dialog, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Ok")); + Button wCancel = new Button(dialog, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> dialog.dispose()); + wOk.addListener( + SWT.Selection, + e -> { + if (StringUtils.isAnyBlank(wGroup.getText(), wArt.getText(), wVer.getText())) { + MessageBox box = new MessageBox(dialog, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.Header")); + box.setMessage(BaseMessages.getString(PKG, "HopEnvironmentDialog.Validation.DepGav")); + box.open(); + return; + } + ref.setGroupId(wGroup.getText().trim()); + ref.setArtifactId(wArt.getText().trim()); + ref.setVersion(wVer.getText().trim()); + ref.setTarget(StringUtils.defaultIfBlank(wTarget.getText(), "lib/jdbc").trim()); + ok[0] = true; + dialog.dispose(); + }); + BaseTransformDialog.positionBottomButtons(dialog, new Button[] {wOk, wCancel}, margin, wTarget); + BaseTransformDialog.setSize(dialog); + dialog.open(); + Display display = shell.getDisplay(); + while (!dialog.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + return ok[0]; + } + + // --- table helpers --- + + private void removeSelected(TableView view) { + int idx = view.table.getSelectionIndex(); + if (idx < 0) { + return; + } + view.table.remove(idx); + markDirty(); + } + + private void moveSelected(TableView view, int delta) { + int idx = view.table.getSelectionIndex(); + if (idx < 0) { + return; + } + int target = idx + delta; + if (target < 0 || target >= view.table.getItemCount()) { + return; + } + TableItem a = view.table.getItem(idx); + TableItem b = view.table.getItem(target); + Object dataA = a.getData(); + Object dataB = b.getData(); + int cols = view.table.getColumnCount(); + String[] textsA = new String[cols]; + String[] textsB = new String[cols]; + for (int c = 0; c < cols; c++) { + textsA[c] = a.getText(c); + textsB[c] = b.getText(c); + } + for (int c = 0; c < cols; c++) { + a.setText(c, textsB[c]); + b.setText(c, textsA[c]); + } + a.setData(dataB); + b.setData(dataA); + view.table.setSelection(target); + markDirty(); + } + + private Label labeled(Shell dialog, String key, Object above, int middle, int margin) { + Label label = new Label(dialog, SWT.RIGHT); + PropsUi.setLook(label); + label.setText(BaseMessages.getString(PKG, key)); + FormData fd = new FormData(); + fd.left = new FormAttachment(0, 0); + fd.right = new FormAttachment(middle, -margin); + if (above instanceof Integer) { + fd.top = new FormAttachment(0, margin); + } else { + fd.top = new FormAttachment((Control) above, margin); + } + label.setLayoutData(fd); + return label; + } + + private Text textField(Shell dialog, Label label, int middle, String value) { + Text text = new Text(dialog, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.setText(value); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(label, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void notifyStatus(String message) { + if (statusListener != null && message != null) { + statusListener.accept(message); + } + } + + private static String blankToNull(String value) { + return StringUtils.isBlank(value) ? null : value.trim(); + } + + public boolean wasSaved() { + return saved; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java new file mode 100644 index 00000000000..2da895b9704 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceDialog.java @@ -0,0 +1,582 @@ +/* + * 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.marketplace.gui; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.marketplace.catalog.OptionalPluginCatalog; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.marketplace.catalog.PluginDiscovery; +import org.apache.hop.marketplace.command.MarketplaceCommand; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.env.EnvironmentApplier; +import org.apache.hop.marketplace.install.HopHome; +import org.apache.hop.marketplace.install.InstallReceipt; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.apache.hop.marketplace.install.PluginUninstaller; +import org.apache.hop.marketplace.resolve.MavenCoordinates; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.gui.GuiResource; +import org.apache.hop.ui.core.gui.WindowProperty; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.custom.CTabItem; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +/** Tools → Marketplace dialog: list optional plugins and install/uninstall from a Maven repo. */ +public class MarketplaceDialog extends Dialog { + + private static final Class PKG = MarketplaceGuiPlugin.class; + + private final PropsUi props; + private final ILogChannel log = new LogChannel("Marketplace"); + + private Shell shell; + private TableView wTable; + private Text wSearch; + private Label wStatus; + private Path hopHome; + private MarketplaceConfig config; + private MarketplaceRepositoriesPanel repositoriesPanel; + + public MarketplaceDialog(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + this.props = PropsUi.getInstance(); + } + + public void open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + PropsUi.setLook(shell); + shell.setImage(GuiResource.getInstance().getImageHopUi()); + shell.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Shell.Title")); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + + try { + hopHome = HopHome.resolve(); + config = MarketplaceConfig.load(); + } catch (Exception e) { + new ErrorDialog( + parent, + BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.Error.ResolveHome"), + e); + return; + } + + // Shell-level Close only; Install/Uninstall/Refresh live on the Plugins tab. + Button wClose = new Button(shell, SWT.PUSH); + wClose.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Button.Close")); + // shell.close() → ShellAdapter.shellClosed (single dirty check for button and window X) + wClose.addListener(SWT.Selection, e -> shell.close()); + BaseTransformDialog.positionBottomButtons( + shell, new Button[] {wClose}, PropsUi.getMargin(), null); + + // Status (shared across tabs) + wStatus = new Label(shell, SWT.LEFT); + PropsUi.setLook(wStatus); + wStatus.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Status.RestartHint")); + FormData fdStatus = new FormData(); + fdStatus.left = new FormAttachment(0, 0); + fdStatus.right = new FormAttachment(100, 0); + fdStatus.bottom = new FormAttachment(wClose, -PropsUi.getMargin() * 2); + wStatus.setLayoutData(fdStatus); + + 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(wStatus, -PropsUi.getMargin()); + wTabFolder.setLayoutData(fdTabs); + + createPluginsTab(wTabFolder); + createRepositoriesTab(wTabFolder); + createEnvironmentTab(wTabFolder); + wTabFolder.setSelection(0); + + shell.addShellListener( + new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + // X / Alt+F4 — same dirty prompt as Close button + if (!confirmClose()) { + e.doit = false; + } else { + props.setScreen(new WindowProperty(shell)); + } + } + }); + + refreshTable(false); + + BaseTransformDialog.setSize(shell); + shell.open(); + Display display = parent.getDisplay(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } + + private void createPluginsTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Tab.Plugins")); + tab.setImage(GuiResource.getInstance().getImagePlugin()); + 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); + + Button wUninstall = new Button(comp, SWT.PUSH); + wUninstall.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Button.Uninstall")); + wUninstall.addListener(SWT.Selection, e -> uninstallSelected()); + + Button wInstall = new Button(comp, SWT.PUSH); + wInstall.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Button.Install")); + wInstall.addListener(SWT.Selection, e -> installSelected()); + + Button wRefresh = new Button(comp, SWT.PUSH); + wRefresh.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Button.Refresh")); + wRefresh.addListener(SWT.Selection, e -> refreshTable(true)); + + BaseTransformDialog.positionBottomButtons( + comp, new Button[] {wInstall, wUninstall, wRefresh}, PropsUi.getMargin(), null); + + // Search / filter above the plugin list + Label wlSearch = new Label(comp, SWT.RIGHT); + PropsUi.setLook(wlSearch); + wlSearch.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Search.Label")); + FormData fdlSearch = new FormData(); + fdlSearch.left = new FormAttachment(0, 0); + fdlSearch.top = new FormAttachment(0, 0); + wlSearch.setLayoutData(fdlSearch); + + Button wClearSearch = new Button(comp, SWT.PUSH); + wClearSearch.setImage(GuiResource.getInstance().getImageClear()); + wClearSearch.setToolTipText(BaseMessages.getString(PKG, "MarketplaceDialog.Search.Clear")); + wClearSearch.addListener(SWT.Selection, e -> clearSearch()); + FormData fdClearSearch = new FormData(); + fdClearSearch.right = new FormAttachment(100, 0); + fdClearSearch.top = new FormAttachment(wlSearch, 0, SWT.CENTER); + wClearSearch.setLayoutData(fdClearSearch); + + wSearch = new Text(comp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wSearch); + wSearch.setMessage(BaseMessages.getString(PKG, "MarketplaceDialog.Search.Placeholder")); + FormData fdSearch = new FormData(); + fdSearch.left = new FormAttachment(wlSearch, PropsUi.getMargin()); + fdSearch.top = new FormAttachment(wlSearch, 0, SWT.CENTER); + fdSearch.right = new FormAttachment(wClearSearch, -PropsUi.getMargin()); + wSearch.setLayoutData(fdSearch); + wSearch.addListener(SWT.Modify, e -> refreshTable(false)); + + Label wSearchSep = new Label(comp, SWT.HORIZONTAL | SWT.SEPARATOR); + FormData fdSearchSep = new FormData(); + fdSearchSep.left = new FormAttachment(0, 0); + fdSearchSep.right = new FormAttachment(100, 0); + fdSearchSep.top = new FormAttachment(wSearch, PropsUi.getMargin()); + wSearchSep.setLayoutData(fdSearchSep); + + ColumnInfo[] columns = { + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Version"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Artifact"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Category"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Repository"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Status"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "MarketplaceDialog.Column.Description"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }; + wTable = + new TableView( + Variables.getADefaultVariableSpace(), + comp, + SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE | SWT.V_SCROLL, + columns, + 1, + true, + null, + props, + false); + PropsUi.setLook(wTable); + + FormData fdTable = new FormData(); + fdTable.left = new FormAttachment(0, 0); + fdTable.top = new FormAttachment(wSearchSep, PropsUi.getMargin()); + fdTable.right = new FormAttachment(100, 0); + fdTable.bottom = new FormAttachment(wInstall, -PropsUi.getMargin() * 2); + wTable.setLayoutData(fdTable); + } + + private void createEnvironmentTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Tab.Environment")); + tab.setImage(GuiResource.getInstance().getImageFile()); + 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); + + // Full hop-env editor (formerly HopEnvironmentDialog modal) + HopEnvironmentDialog.embed( + comp, + resolveDefaultEnvPath(), + msg -> { + if (wStatus != null && !wStatus.isDisposed()) { + wStatus.setText(msg); + } + }); + } + + private Path resolveDefaultEnvPath() { + // Last file edited in the marketplace environment editor (audit list, most-recent first) + String last = + org.apache.hop.ui.hopgui.shared.AuditManagerGuiUtil.getLastUsedValue( + HopEnvironmentDialog.AUDIT_TYPE_ENV_FILES); + if (StringUtils.isNotBlank(last)) { + Path lastPath = Path.of(last.trim()).toAbsolutePath().normalize(); + if (Files.isRegularFile(lastPath)) { + return lastPath; + } + } + Path fullClient = hopHome.resolve("full-client-env.yaml"); + if (Files.isRegularFile(fullClient)) { + return fullClient; + } + return EnvironmentApplier.resolveEnvironmentFile(hopHome, null); + } + + private void createRepositoriesTab(CTabFolder folder) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Tab.Repositories")); + tab.setImage(GuiResource.getInstance().getImageServer()); + 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); + + // Full repository management UI (formerly a nested ManageRepositoriesDialog) + repositoriesPanel = new MarketplaceRepositoriesPanel(comp, config); + } + + private void clearSearch() { + if (wSearch != null && !wSearch.isDisposed()) { + wSearch.setText(""); + wSearch.setFocus(); + } + } + + /** + * Reload the plugins list. + * + * @param userRefresh when true (Refresh button), prompt to save dirty repositories and show a + * wait cursor; search filter updates pass false. + */ + private void refreshTable(boolean userRefresh) { + if (userRefresh && !ensureRepositoriesSavedForRefresh()) { + return; + } + + Cursor waitCursor = null; + if (userRefresh && shell != null && !shell.isDisposed()) { + waitCursor = shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT); + shell.setCursor(waitCursor); + } + try { + // Keep shared in-memory config (after optional save/discard). Do not reload from disk on + // every search keystroke — that would drop unsaved repository edits. + if (repositoriesPanel != null) { + config = repositoriesPanel.getConfig(); + } + wTable.table.removeAll(); + // Filter only when more than 2 characters are typed (same as explorer/metadata search). + String searchText = wSearch != null ? wSearch.getText() : ""; + String filter = + searchText != null && searchText.trim().length() > 2 ? searchText.trim() : null; + // Apache optional catalog + live list from every browse=true repository + List plugins = PluginDiscovery.query(filter, null, config, log); + for (OptionalPluginInfo info : plugins) { + // Column 0 is the (hidden) index column; data columns are 1-based. + TableItem item = new TableItem(wTable.table, SWT.NONE); + String name = Const.NVL(info.getName(), info.getArtifactId()); + item.setText(1, Const.NVL(name, "")); + item.setText(2, Const.NVL(info.getVersion(), "")); + item.setText(3, Const.NVL(info.getArtifactId(), "")); + item.setText(4, Const.NVL(info.getCategory(), "")); + item.setText(5, repositoryDisplayName(info.getSource())); + boolean onDisk = OptionalPluginCatalog.isInstalledOnDisk(hopHome, info); + InstallReceipt receipt = null; + try { + receipt = PluginInstaller.readReceipt(hopHome, info.getArtifactId()); + } catch (Exception e) { + log.logError("Unable to read receipt for " + info.getArtifactId(), e); + } + String status; + if (onDisk && receipt != null) { + status = + BaseMessages.getString(PKG, "MarketplaceDialog.Status.Installed") + + " (" + + receipt.getVersion() + + ")"; + } else if (onDisk) { + status = BaseMessages.getString(PKG, "MarketplaceDialog.Status.Present"); + } else { + status = BaseMessages.getString(PKG, "MarketplaceDialog.Status.NotInstalled"); + } + item.setText(6, status); + item.setText(7, Const.NVL(info.getDescription(), "")); + item.setData(info); + } + wTable.optimizeTableView(); + } finally { + if (userRefresh && shell != null && !shell.isDisposed()) { + shell.setCursor(null); + } + } + } + + /** + * If repository edits are unsaved, ask the user to save before refreshing plugins (discovery uses + * the config that is saved / in memory). + * + * @return false if the user cancelled + */ + private boolean ensureRepositoriesSavedForRefresh() { + if (repositoriesPanel == null || !repositoriesPanel.isDirty()) { + return true; + } + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.CANCEL | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.ReposDirty.Header")); + box.setMessage(BaseMessages.getString(PKG, "MarketplaceDialog.ReposDirty.Refresh.Message")); + int answer = box.open(); + if (answer == SWT.CANCEL) { + return false; + } + if (answer == SWT.YES) { + return repositoriesPanel.saveChanges(false); + } + // No — discard in-memory edits and reload hop-config so refresh matches disk + try { + MarketplaceConfig loaded = MarketplaceConfig.load(); + config = loaded; + repositoriesPanel.setConfig(loaded); + return true; + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.ReposDirty.Reload.Error"), + e); + return false; + } + } + + /** + * @return true if the dialog may close (saved, discarded, or clean) + */ + private boolean confirmClose() { + if (repositoriesPanel == null || !repositoriesPanel.isDirty()) { + return true; + } + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.CANCEL | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.ReposDirty.Header")); + box.setMessage(BaseMessages.getString(PKG, "MarketplaceDialog.ReposDirty.Close.Message")); + int answer = box.open(); + if (answer == SWT.CANCEL) { + return false; + } + if (answer == SWT.YES) { + return repositoriesPanel.saveChanges(false); + } + // No — discard + return true; + } + + private OptionalPluginInfo selected() { + TableItem[] items = wTable.table.getSelection(); + if (items == null || items.length == 0) { + return null; + } + return (OptionalPluginInfo) items[0].getData(); + } + + /** + * Human-readable origin for a discovery hit: repository display name when {@code source} is a + * known repo id, otherwise the source string (e.g. {@code apache}). + */ + private String repositoryDisplayName(String source) { + if (StringUtils.isBlank(source)) { + return ""; + } + if ("apache".equalsIgnoreCase(source)) { + return BaseMessages.getString(PKG, "MarketplaceDialog.Repository.Apache"); + } + if (config != null) { + MarketplaceRepository repo = config.findRepository(source); + if (repo != null) { + return Const.NVL(repo.displayName(), source); + } + } + return source; + } + + private void installSelected() { + OptionalPluginInfo info = selected(); + if (info == null) { + return; + } + try { + if (repositoriesPanel != null) { + config = repositoriesPanel.getConfig(); + } + String groupId = + StringUtils.isNotBlank(info.getGroupId()) ? info.getGroupId() : config.getGroupId(); + String version = + StringUtils.isNotBlank(info.getVersion()) + ? info.getVersion() + : MarketplaceCommand.resolveDefaultVersion(config); + MavenCoordinates coords = new MavenCoordinates(groupId, info.getArtifactId(), version); + String preferredRepo = + StringUtils.isNotBlank(info.getSource()) && !"apache".equalsIgnoreCase(info.getSource()) + ? info.getSource() + : null; + wStatus.setText( + BaseMessages.getString(PKG, "MarketplaceDialog.Status.Installing", coords.gav())); + shell.update(); + new PluginInstaller(log, hopHome, config).install(coords, true, null, preferredRepo); + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Install.Done.Header")); + box.setMessage( + BaseMessages.getString(PKG, "MarketplaceDialog.Install.Done.Message", coords.gav())); + box.open(); + refreshTable(false); + wStatus.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Status.RestartHint")); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.Install.Error", info.getArtifactId()), + e); + } + } + + private void uninstallSelected() { + OptionalPluginInfo info = selected(); + if (info == null) { + return; + } + try { + InstallReceipt receipt = PluginInstaller.readReceipt(hopHome, info.getArtifactId()); + if (receipt == null) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Uninstall.NoReceipt.Header")); + box.setMessage( + BaseMessages.getString( + PKG, "MarketplaceDialog.Uninstall.NoReceipt.Message", info.getArtifactId())); + box.open(); + return; + } + MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + confirm.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Uninstall.Confirm.Header")); + confirm.setMessage( + BaseMessages.getString( + PKG, "MarketplaceDialog.Uninstall.Confirm.Message", info.getArtifactId())); + if (confirm.open() != SWT.YES) { + return; + } + new PluginUninstaller(log, hopHome).uninstall(info.getArtifactId()); + refreshTable(false); + wStatus.setText(BaseMessages.getString(PKG, "MarketplaceDialog.Status.RestartHint")); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "MarketplaceDialog.Error.Header"), + BaseMessages.getString(PKG, "MarketplaceDialog.Uninstall.Error", info.getArtifactId()), + e); + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceGuiPlugin.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceGuiPlugin.java new file mode 100644 index 00000000000..7ca82d0ce92 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceGuiPlugin.java @@ -0,0 +1,71 @@ +/* + * 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.marketplace.gui; + +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.menu.GuiMenuElement; +import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.hopgui.HopGui; + +/** GUI entry points for the Hop plugin marketplace. */ +@GuiPlugin +public class MarketplaceGuiPlugin { + + public static final Class PKG = MarketplaceGuiPlugin.class; + + public static final String ID_MAIN_MENU_TOOLS_MARKETPLACE = "40200-menu-tools-marketplace"; + + /** After Save As ({@code toolbar-10050-save-as}) on the main toolbar. */ + public static final String ID_MAIN_TOOLBAR_MARKETPLACE = "toolbar-10060-marketplace"; + + private static MarketplaceGuiPlugin instance; + + public MarketplaceGuiPlugin() { + // Instantiated by the GUI plugin system + } + + public static MarketplaceGuiPlugin getInstance() { + if (instance == null) { + instance = new MarketplaceGuiPlugin(); + } + return instance; + } + + @GuiMenuElement( + root = HopGui.ID_MAIN_MENU, + id = ID_MAIN_MENU_TOOLS_MARKETPLACE, + label = "i18n::MarketplaceGuiPlugin.Menu.Marketplace.Text", + parentId = HopGui.ID_MAIN_MENU_TOOLS_PARENT_ID, + image = "ui/images/plugin.svg", + separator = true) + @GuiToolbarElement( + root = HopGui.ID_MAIN_TOOLBAR, + id = ID_MAIN_TOOLBAR_MARKETPLACE, + image = "ui/images/plugin.svg", + toolTip = "i18n::MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip", + separator = true) + public void menuToolsMarketplace() { + HopGui hopGui = HopGui.getInstance(); + try { + new MarketplaceDialog(hopGui.getShell()).open(); + } catch (Exception e) { + new ErrorDialog(hopGui.getShell(), "Marketplace", "Unable to open the marketplace dialog", e); + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceRepositoriesPanel.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceRepositoriesPanel.java new file mode 100644 index 00000000000..824db2adeec --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/MarketplaceRepositoriesPanel.java @@ -0,0 +1,896 @@ +/* + * 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.marketplace.gui; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.marketplace.catalog.OptionalPluginInfo; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.config.MarketplaceRepositoryDefinition; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.gui.WindowProperty; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +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; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +/** + * Marketplace repository list editor hosted as a panel (e.g. Marketplace dialog Repositories tab). + * Settings are saved to hop-config.json via {@link MarketplaceConfig#save()}. + * + *

Plugin metadata for a repository is edited in the Add/Edit repository dialog (Plugin metadata + * tab). Import/Export shareable hop-marketplace-repo YAML definitions from this panel. + */ +public class MarketplaceRepositoriesPanel { + + private static final Class PKG = MarketplaceGuiPlugin.class; + + private MarketplaceConfig config; + private final Shell shell; + private final TableView wTable; + private boolean dirty; + + /** + * Build the repositories UI into {@code parent} (must use {@link FormLayout}). + * + * @param parent tab composite + * @param config shared marketplace config (mutated in place; call Save to persist) + */ + public MarketplaceRepositoriesPanel(Composite parent, MarketplaceConfig config) { + PropsUi props = PropsUi.getInstance(); + this.config = config; + this.shell = parent.getShell(); + this.dirty = false; + + Label wlHelp = new Label(parent, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + Button wSave = createRightButton(parent, "ManageRepositoriesDialog.Button.Save"); + wSave.addListener(SWT.Selection, e -> save()); + Button wReset = createRightButton(parent, "ManageRepositoriesDialog.Button.ResetDefaults"); + wReset.addListener(SWT.Selection, e -> resetDefaults()); + Button wDown = createRightButton(parent, "ManageRepositoriesDialog.Button.MoveDown"); + wDown.addListener(SWT.Selection, e -> moveSelected(1)); + Button wUp = createRightButton(parent, "ManageRepositoriesDialog.Button.MoveUp"); + wUp.addListener(SWT.Selection, e -> moveSelected(-1)); + Button wPrimary = createRightButton(parent, "ManageRepositoriesDialog.Button.SetPrimary"); + wPrimary.addListener(SWT.Selection, e -> setPrimarySelected()); + Button wExport = createRightButton(parent, "ManageRepositoriesDialog.Button.Export"); + wExport.addListener(SWT.Selection, e -> exportDefinition()); + Button wImport = createRightButton(parent, "ManageRepositoriesDialog.Button.Import"); + wImport.addListener(SWT.Selection, e -> importDefinition()); + Button wRemove = createRightButton(parent, "ManageRepositoriesDialog.Button.Remove"); + wRemove.addListener(SWT.Selection, e -> removeSelected()); + Button wEdit = createRightButton(parent, "ManageRepositoriesDialog.Button.Edit"); + wEdit.addListener(SWT.Selection, e -> editSelected()); + Button wAdd = createRightButton(parent, "ManageRepositoriesDialog.Button.Add"); + wAdd.addListener(SWT.Selection, e -> addRepository()); + + Button[] rightButtons = { + wAdd, wEdit, wRemove, wImport, wExport, wPrimary, wUp, wDown, wReset, wSave + }; + layoutRightButtons(rightButtons, wlHelp); + + ColumnInfo[] columns = { + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Primary"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Enabled"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Browse"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Id"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Url"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Column.Auth"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + true), + }; + wTable = + new TableView( + Variables.getADefaultVariableSpace(), + parent, + SWT.BORDER | SWT.FULL_SELECTION | SWT.SINGLE | SWT.V_SCROLL, + columns, + 1, + true, + null, + props, + false); + PropsUi.setLook(wTable); + FormData fdTable = new FormData(); + fdTable.left = new FormAttachment(0, 0); + fdTable.top = new FormAttachment(wlHelp, PropsUi.getMargin() * 2); + fdTable.right = new FormAttachment(wAdd, -PropsUi.getMargin()); + fdTable.bottom = new FormAttachment(100, 0); + wTable.setLayoutData(fdTable); + + refreshTable(); + } + + private static Button createRightButton(Composite parent, String messageKey) { + Button b = new Button(parent, SWT.PUSH); + b.setText(BaseMessages.getString(PKG, messageKey)); + PropsUi.setLook(b); + return b; + } + + private void layoutRightButtons(Button[] buttons, Label wlHelp) { + int margin = PropsUi.getMargin(); + int maxW = 80; + for (Button b : buttons) { + maxW = Math.max(maxW, b.computeSize(SWT.DEFAULT, SWT.DEFAULT).x); + } + Button prev = null; + for (Button b : buttons) { + FormData fd = new FormData(); + fd.right = new FormAttachment(100, 0); + fd.left = new FormAttachment(100, -maxW - 8); + if (prev == null) { + fd.top = new FormAttachment(wlHelp, margin * 2); + } else { + fd.top = new FormAttachment(prev, margin); + } + b.setLayoutData(fd); + prev = b; + } + } + + public boolean isDirty() { + return dirty; + } + + public boolean saveChanges(boolean showSuccessDialog) { + try { + config.ensureValidPrimary(); + config.save(); + dirty = false; + if (showSuccessDialog) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Save.Done.Header")); + box.setMessage(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Save.Done.Message")); + box.open(); + } + return true; + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Save"), + e); + return false; + } + } + + public void setConfig(MarketplaceConfig config) { + this.config = config; + this.dirty = false; + refreshTable(); + } + + public MarketplaceConfig getConfig() { + return config; + } + + private void markDirty() { + dirty = true; + } + + private void refreshTable() { + wTable.table.removeAll(); + if (config.getRepositories() == null) { + wTable.optimizeTableView(); + return; + } + for (MarketplaceRepository repo : config.getRepositories()) { + if (repo == null) { + continue; + } + TableItem item = new TableItem(wTable.table, SWT.NONE); + item.setText(1, repo.isPrimary() ? "*" : ""); + item.setText(2, repo.isEnabled() ? "Y" : "N"); + item.setText(3, repo.isBrowse() ? "Y" : "N"); + item.setText(4, Const.NVL(repo.getId(), "")); + item.setText(5, Const.NVL(repo.displayName(), "")); + item.setText(6, Const.NVL(repo.getUrl(), "")); + item.setText( + 7, repo.hasCredentials() || StringUtils.isNotBlank(repo.getUsername()) ? "Y" : ""); + item.setData(repo); + } + wTable.optimizeTableView(); + } + + private MarketplaceRepository selected() { + TableItem[] items = wTable.table.getSelection(); + if (items == null || items.length == 0) { + return null; + } + return (MarketplaceRepository) items[0].getData(); + } + + private void addRepository() { + MarketplaceRepository repo = new MarketplaceRepository(); + repo.setId(""); + repo.setName(""); + repo.setUrl(""); + repo.setEnabled(true); + if (editRepository(repo, true)) { + try { + config.addRepository(repo); + markDirty(); + refreshTable(); + selectRepoId(repo.getId()); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + e.getMessage(), + e); + } + } + } + + private void editSelected() { + MarketplaceRepository repo = selected(); + if (repo == null) { + return; + } + if (editRepository(repo, false)) { + config.ensureValidPrimary(); + markDirty(); + refreshTable(); + selectRepoId(repo.getId()); + } + } + + private void selectRepoId(String id) { + if (StringUtils.isBlank(id) || config.getRepositories() == null) { + return; + } + for (int i = 0; i < wTable.table.getItemCount(); i++) { + MarketplaceRepository r = (MarketplaceRepository) wTable.table.getItem(i).getData(); + if (r != null && id.equals(r.getId())) { + wTable.table.setSelection(i); + break; + } + } + } + + /** + * Add/Edit repository dialog: General connection fields + Plugin metadata table (for this repo + * only). Mutations applied only on OK. + */ + private boolean editRepository(MarketplaceRepository repo, boolean isNew) { + Shell dialog = new Shell(shell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | SWT.RESIZE | SWT.MAX); + PropsUi props = PropsUi.getInstance(); + PropsUi.setLook(dialog); + dialog.setText( + BaseMessages.getString( + PKG, + isNew + ? "ManageRepositoriesDialog.Edit.Title.Add" + : "ManageRepositoriesDialog.Edit.Title.Edit")); + FormLayout layout = new FormLayout(); + layout.marginWidth = PropsUi.getFormMargin(); + layout.marginHeight = PropsUi.getFormMargin(); + dialog.setLayout(layout); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + final boolean[] ok = {false}; + + Button wOk = new Button(dialog, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Button.Ok")); + Button wCancel = new Button(dialog, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Button.Cancel")); + BaseTransformDialog.positionBottomButtons(dialog, new Button[] {wOk, wCancel}, margin, null); + + CTabFolder tabs = new CTabFolder(dialog, SWT.BORDER); + PropsUi.setLook(tabs); + 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); + tabs.setLayoutData(fdTabs); + + // ----- General ----- + CTabItem generalTab = new CTabItem(tabs, SWT.NONE); + generalTab.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Tab.General")); + Composite general = new Composite(tabs, SWT.NONE); + PropsUi.setLook(general); + general.setLayout(new FormLayout()); + generalTab.setControl(general); + + Label wlId = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlId); + wlId.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Id")); + FormData fdlId = new FormData(); + fdlId.left = new FormAttachment(0, 0); + fdlId.top = new FormAttachment(0, margin); + fdlId.right = new FormAttachment(middle, -margin); + wlId.setLayoutData(fdlId); + Text wId = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wId); + wId.setText(Const.NVL(repo.getId(), "")); + wId.setEditable(isNew); + FormData fdId = new FormData(); + fdId.left = new FormAttachment(middle, margin); + fdId.top = new FormAttachment(wlId, 0, SWT.CENTER); + fdId.right = new FormAttachment(100, 0); + wId.setLayoutData(fdId); + + Label wlName = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlName); + wlName.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Name")); + FormData fdlName = new FormData(); + fdlName.left = new FormAttachment(0, 0); + fdlName.top = new FormAttachment(wId, margin); + fdlName.right = new FormAttachment(middle, -margin); + wlName.setLayoutData(fdlName); + Text wName = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + wName.setText(Const.NVL(repo.getName(), "")); + FormData fdName = new FormData(); + fdName.left = new FormAttachment(middle, margin); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.right = new FormAttachment(100, 0); + wName.setLayoutData(fdName); + + Label wlUrl = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlUrl); + wlUrl.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Url")); + FormData fdlUrl = new FormData(); + fdlUrl.left = new FormAttachment(0, 0); + fdlUrl.top = new FormAttachment(wName, margin); + fdlUrl.right = new FormAttachment(middle, -margin); + wlUrl.setLayoutData(fdlUrl); + Text wUrl = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wUrl); + wUrl.setText(Const.NVL(repo.getUrl(), "")); + FormData fdUrl = new FormData(); + fdUrl.left = new FormAttachment(middle, margin); + fdUrl.top = new FormAttachment(wlUrl, 0, SWT.CENTER); + fdUrl.right = new FormAttachment(100, 0); + wUrl.setLayoutData(fdUrl); + + Label wlUser = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlUser); + wlUser.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Username")); + FormData fdlUser = new FormData(); + fdlUser.left = new FormAttachment(0, 0); + fdlUser.top = new FormAttachment(wUrl, margin); + fdlUser.right = new FormAttachment(middle, -margin); + wlUser.setLayoutData(fdlUser); + Text wUser = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wUser); + wUser.setText(Const.NVL(repo.getUsername(), "")); + FormData fdUser = new FormData(); + fdUser.left = new FormAttachment(middle, margin); + fdUser.top = new FormAttachment(wlUser, 0, SWT.CENTER); + fdUser.right = new FormAttachment(100, 0); + wUser.setLayoutData(fdUser); + + Label wlPass = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlPass); + wlPass.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Password")); + FormData fdlPass = new FormData(); + fdlPass.left = new FormAttachment(0, 0); + fdlPass.top = new FormAttachment(wUser, margin); + fdlPass.right = new FormAttachment(middle, -margin); + wlPass.setLayoutData(fdlPass); + Text wPass = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.PASSWORD); + PropsUi.setLook(wPass); + FormData fdPass = new FormData(); + fdPass.left = new FormAttachment(middle, margin); + fdPass.top = new FormAttachment(wlPass, 0, SWT.CENTER); + fdPass.right = new FormAttachment(100, 0); + wPass.setLayoutData(fdPass); + + Button wEnabled = new Button(general, SWT.CHECK); + PropsUi.setLook(wEnabled); + wEnabled.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Enabled")); + wEnabled.setSelection(repo.isEnabled()); + FormData fdEnabled = new FormData(); + fdEnabled.left = new FormAttachment(middle, margin); + fdEnabled.top = new FormAttachment(wPass, margin); + wEnabled.setLayoutData(fdEnabled); + + Button wPrimary = new Button(general, SWT.CHECK); + PropsUi.setLook(wPrimary); + wPrimary.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Primary")); + wPrimary.setSelection(repo.isPrimary()); + FormData fdPrimary = new FormData(); + fdPrimary.left = new FormAttachment(middle, margin); + fdPrimary.top = new FormAttachment(wEnabled, margin); + wPrimary.setLayoutData(fdPrimary); + + Button wBrowse = new Button(general, SWT.CHECK); + PropsUi.setLook(wBrowse); + wBrowse.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Browse")); + wBrowse.setToolTipText( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.Browse.Tooltip")); + wBrowse.setSelection(repo.isBrowse()); + FormData fdBrowse = new FormData(); + fdBrowse.left = new FormAttachment(middle, margin); + fdBrowse.top = new FormAttachment(wPrimary, margin); + wBrowse.setLayoutData(fdBrowse); + + Label wlCatalog = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlCatalog); + wlCatalog.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.CatalogUrl")); + FormData fdlCatalog = new FormData(); + fdlCatalog.left = new FormAttachment(0, 0); + fdlCatalog.top = new FormAttachment(wBrowse, margin); + fdlCatalog.right = new FormAttachment(middle, -margin); + wlCatalog.setLayoutData(fdlCatalog); + Text wCatalog = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wCatalog); + wCatalog.setText(Const.NVL(repo.getCatalogUrl(), "")); + FormData fdCatalog = new FormData(); + fdCatalog.left = new FormAttachment(middle, margin); + fdCatalog.top = new FormAttachment(wlCatalog, 0, SWT.CENTER); + fdCatalog.right = new FormAttachment(100, 0); + wCatalog.setLayoutData(fdCatalog); + + Label wlSearch = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlSearch); + wlSearch.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.SearchQuery")); + FormData fdlSearch = new FormData(); + fdlSearch.left = new FormAttachment(0, 0); + fdlSearch.top = new FormAttachment(wCatalog, margin); + fdlSearch.right = new FormAttachment(middle, -margin); + wlSearch.setLayoutData(fdlSearch); + Text wSearch = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wSearch); + wSearch.setText(Const.NVL(repo.getSearchQuery(), "")); + FormData fdSearch = new FormData(); + fdSearch.left = new FormAttachment(middle, margin); + fdSearch.top = new FormAttachment(wlSearch, 0, SWT.CENTER); + fdSearch.right = new FormAttachment(100, 0); + wSearch.setLayoutData(fdSearch); + + Label wlGroup = new Label(general, SWT.RIGHT); + PropsUi.setLook(wlGroup); + wlGroup.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.GroupIdFilter")); + FormData fdlGroup = new FormData(); + fdlGroup.left = new FormAttachment(0, 0); + fdlGroup.top = new FormAttachment(wSearch, margin); + fdlGroup.right = new FormAttachment(middle, -margin); + wlGroup.setLayoutData(fdlGroup); + Text wGroup = new Text(general, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wGroup); + wGroup.setText(Const.NVL(repo.getGroupIdFilter(), "")); + FormData fdGroup = new FormData(); + fdGroup.left = new FormAttachment(middle, margin); + fdGroup.top = new FormAttachment(wlGroup, 0, SWT.CENTER); + fdGroup.right = new FormAttachment(100, 0); + wGroup.setLayoutData(fdGroup); + + Button wSnapshots = new Button(general, SWT.CHECK); + PropsUi.setLook(wSnapshots); + wSnapshots.setText( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.IncludeSnapshots")); + wSnapshots.setSelection(repo.isIncludeSnapshots()); + FormData fdSnapshots = new FormData(); + fdSnapshots.left = new FormAttachment(middle, margin); + fdSnapshots.top = new FormAttachment(wGroup, margin); + wSnapshots.setLayoutData(fdSnapshots); + + // ----- Plugin metadata ----- + CTabItem metaTab = new CTabItem(tabs, SWT.NONE); + metaTab.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Tab.PluginMetadata")); + Composite meta = new Composite(tabs, SWT.NONE); + PropsUi.setLook(meta); + meta.setLayout(new FormLayout()); + metaTab.setControl(meta); + + Label wlMetaHelp = new Label(meta, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlMetaHelp); + wlMetaHelp.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Hint")); + FormData fdlMetaHelp = new FormData(); + fdlMetaHelp.left = new FormAttachment(0, 0); + fdlMetaHelp.top = new FormAttachment(0, 0); + fdlMetaHelp.right = new FormAttachment(100, 0); + wlMetaHelp.setLayoutData(fdlMetaHelp); + + Button wAddPlugin = new Button(meta, SWT.PUSH); + wAddPlugin.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Button.AddPlugin")); + Button wRemovePlugin = new Button(meta, SWT.PUSH); + wRemovePlugin.setText( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Button.RemovePlugin")); + BaseTransformDialog.positionBottomButtons( + meta, new Button[] {wAddPlugin, wRemovePlugin}, margin, null); + + ColumnInfo[] pluginCols = { + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Group"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Artifact"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Version"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Category"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.Description"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.MinHop"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Plugins.Column.MaxHop"), + ColumnInfo.COLUMN_TYPE_TEXT, + false, + false), + }; + TableView wPluginsTable = + new TableView( + Variables.getADefaultVariableSpace(), + meta, + SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL, + pluginCols, + 1, + true, + null, + props, + true); + PropsUi.setLook(wPluginsTable); + FormData fdPlugins = new FormData(); + fdPlugins.left = new FormAttachment(0, 0); + fdPlugins.top = new FormAttachment(wlMetaHelp, margin); + fdPlugins.right = new FormAttachment(100, 0); + fdPlugins.bottom = new FormAttachment(wAddPlugin, -margin); + wPluginsTable.setLayoutData(fdPlugins); + + // Load existing plugins into the table (do not mutate repo until OK) + if (repo.getPlugins() != null) { + for (OptionalPluginInfo p : repo.getPlugins()) { + if (p == null) { + continue; + } + TableItem item = new TableItem(wPluginsTable.table, SWT.NONE); + item.setText(1, Const.NVL(p.getGroupId(), "")); + item.setText(2, Const.NVL(p.getArtifactId(), "")); + item.setText(3, Const.NVL(p.getVersion(), "")); + item.setText(4, Const.NVL(p.getName(), "")); + item.setText(5, Const.NVL(p.getCategory(), "")); + item.setText(6, Const.NVL(p.getDescription(), "")); + item.setText(7, Const.NVL(p.getMinHopVersion(), "")); + item.setText(8, Const.NVL(p.getMaxHopVersion(), "")); + } + } + wPluginsTable.optimizeTableView(); + + wAddPlugin.addListener( + SWT.Selection, + e -> { + TableItem item = new TableItem(wPluginsTable.table, SWT.NONE); + item.setText(1, "org.apache.hop"); + item.setText(5, "Community"); + wPluginsTable.optimizeTableView(); + }); + wRemovePlugin.addListener( + SWT.Selection, + e -> { + int[] indices = wPluginsTable.table.getSelectionIndices(); + if (indices == null || indices.length == 0) { + return; + } + for (int i = indices.length - 1; i >= 0; i--) { + wPluginsTable.table.remove(indices[i]); + } + }); + + wOk.addListener( + SWT.Selection, + e -> { + if (StringUtils.isBlank(wId.getText()) || StringUtils.isBlank(wUrl.getText())) { + MessageBox box = new MessageBox(dialog, SWT.OK | SWT.ICON_WARNING); + box.setMessage( + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Edit.IdUrlRequired")); + box.open(); + return; + } + repo.setId(wId.getText().trim()); + repo.setName(wName.getText().trim()); + repo.setUrl(wUrl.getText().trim()); + repo.setUsername(StringUtils.trimToNull(wUser.getText())); + if (StringUtils.isNotBlank(wPass.getText())) { + repo.setPassword(wPass.getText()); + } + repo.setEnabled(wEnabled.getSelection()); + repo.setPrimary(wPrimary.getSelection()); + repo.setBrowse(wBrowse.getSelection()); + repo.setCatalogUrl(StringUtils.trimToNull(wCatalog.getText())); + repo.setSearchQuery(StringUtils.trimToNull(wSearch.getText())); + repo.setGroupIdFilter(StringUtils.trimToNull(wGroup.getText())); + repo.setIncludeSnapshots(wSnapshots.getSelection()); + repo.setPlugins(readPluginsFromTable(wPluginsTable, repo.getId())); + ok[0] = true; + PropsUi.getInstance().setScreen(new WindowProperty(dialog)); + dialog.dispose(); + }); + wCancel.addListener(SWT.Selection, e -> dialog.dispose()); + + tabs.setSelection(0); + BaseTransformDialog.setSize(dialog); + // Prefer a taller default so the metadata table is usable + if (dialog.getSize().y < 480) { + dialog.setSize(Math.max(dialog.getSize().x, 640), 520); + } + dialog.open(); + Display display = shell.getDisplay(); + while (!dialog.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + if (ok[0] && repo.isPrimary()) { + for (MarketplaceRepository other : config.getRepositories()) { + if (other != null && other != repo) { + other.setPrimary(false); + } + } + } + return ok[0]; + } + + private static List readPluginsFromTable(TableView table, String repoId) { + List plugins = new ArrayList<>(); + for (int i = 0; i < table.table.getItemCount(); i++) { + TableItem item = table.table.getItem(i); + String artifact = item.getText(2).trim(); + if (StringUtils.isBlank(artifact)) { + continue; + } + OptionalPluginInfo p = new OptionalPluginInfo(); + p.setGroupId(StringUtils.trimToNull(item.getText(1))); + p.setArtifactId(artifact); + p.setVersion(StringUtils.trimToNull(item.getText(3))); + p.setName(StringUtils.trimToNull(item.getText(4))); + p.setCategory(StringUtils.trimToNull(item.getText(5))); + p.setDescription(StringUtils.trimToNull(item.getText(6))); + p.setMinHopVersion(StringUtils.trimToNull(item.getText(7))); + p.setMaxHopVersion(StringUtils.trimToNull(item.getText(8))); + p.setSource(repoId); + plugins.add(p); + } + return plugins; + } + + private void removeSelected() { + MarketplaceRepository repo = selected(); + if (repo == null) { + return; + } + try { + config.removeRepository(repo.getId()); + markDirty(); + refreshTable(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + e.getMessage(), + e); + } + } + + private void importDefinition() { + try { + String path = + BaseDialog.presentFileDialog( + false, + shell, + new String[] {"*.yaml;*.yml;*.json", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Filter.RepoDef"), + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.All") + }, + false); + if (StringUtils.isBlank(path)) { + return; + } + MarketplaceRepository imported = MarketplaceRepositoryDefinition.load(Path.of(path.trim())); + MarketplaceRepositoryDefinition.applyToConfig(config, imported, false); + markDirty(); + refreshTable(); + selectRepoId(imported.getId()); + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Import.Done.Header")); + box.setMessage( + BaseMessages.getString( + PKG, "ManageRepositoriesDialog.Import.Done.Message", imported.getId())); + box.open(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Import.Error"), + e); + } + } + + private void exportDefinition() { + MarketplaceRepository repo = selected(); + if (repo == null) { + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING); + box.setMessage(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Export.Select")); + box.open(); + return; + } + try { + String path = + BaseDialog.presentFileDialog( + true, + shell, + new String[] {"*.yaml;*.yml", "*.json", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Filter.Yaml"), + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Filter.Json"), + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.All") + }, + false); + if (StringUtils.isBlank(path)) { + return; + } + Path out = Path.of(path.trim()); + String name = out.getFileName().toString().toLowerCase(); + if (!name.endsWith(".yaml") && !name.endsWith(".yml") && !name.endsWith(".json")) { + out = out.resolveSibling(out.getFileName().toString() + ".yaml"); + } + MarketplaceRepositoryDefinition.save(out, repo); + MessageBox box = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Export.Done.Header")); + box.setMessage( + BaseMessages.getString( + PKG, "ManageRepositoriesDialog.Export.Done.Message", out.toString())); + box.open(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Export.Error"), + e); + } + } + + private void setPrimarySelected() { + MarketplaceRepository repo = selected(); + if (repo == null) { + return; + } + try { + config.setPrimary(repo.getId()); + markDirty(); + refreshTable(); + selectRepoId(repo.getId()); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ManageRepositoriesDialog.Error.Header"), + e.getMessage(), + e); + } + } + + private void moveSelected(int delta) { + MarketplaceRepository repo = selected(); + if (repo == null || config.getRepositories() == null) { + return; + } + List list = config.getRepositories(); + int idx = list.indexOf(repo); + int target = idx + delta; + if (idx < 0 || target < 0 || target >= list.size()) { + return; + } + list.remove(idx); + list.add(target, repo); + markDirty(); + refreshTable(); + wTable.table.setSelection(target); + } + + private void resetDefaults() { + MessageBox confirm = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + confirm.setText(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Reset.Header")); + confirm.setMessage(BaseMessages.getString(PKG, "ManageRepositoriesDialog.Reset.Message")); + if (confirm.open() != SWT.YES) { + return; + } + config.setRepositories(new ArrayList<>(MarketplaceConfig.defaultRepositories())); + markDirty(); + refreshTable(); + } + + private void save() { + saveChanges(true); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/WelcomeMarketplace.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/WelcomeMarketplace.java new file mode 100644 index 00000000000..ca272ed41a9 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/gui/WelcomeMarketplace.java @@ -0,0 +1,145 @@ +/* + * 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.marketplace.gui; + +import org.apache.hop.core.gui.plugin.GuiElementType; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.GuiWidgetElement; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.gui.GuiCompositeWidgets; +import org.apache.hop.ui.hopgui.HopGui; +import org.apache.hop.ui.hopgui.welcome.WelcomeDialog; +import org.apache.hop.ui.util.EnvironmentUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; + +/** + * Welcome dialog topic for the plugin marketplace: why plugins moved out of the fat client, docs, + * open dialog, and how to restore the full plugin set. + */ +@GuiPlugin +public class WelcomeMarketplace { + + private static final String WELCOME_MARKETPLACE_PARENT_ID = "WelcomeMarketplace.Parent.ID"; + + public static final String WEB_NAME_MARKETPLACE_DOCS = "Marketplace documentation"; + public static final String WEB_LINK_MARKETPLACE_DOCS = + "https://hop.apache.org/manual/latest/hop-tools/hop-marketplace.html"; + + public static final String LINK_OPEN_MARKETPLACE = "Open Marketplace"; + + @GuiWidgetElement( + type = GuiElementType.COMPOSITE, + id = "21000-marketplace-welcome", + label = "Marketplace", + parentId = WelcomeDialog.PARENT_ID_WELCOME_WIDGETS) + public void welcome(Composite parent) { + PropsUi props = PropsUi.getInstance(); + + Composite parentComposite = new Composite(parent, SWT.NONE); + parentComposite.setLayout(props.createFormLayout()); + FormData fdParentComposite = new FormData(); + fdParentComposite.left = new FormAttachment(0, 0); + fdParentComposite.right = new FormAttachment(100, 0); + fdParentComposite.top = new FormAttachment(0, 0); + fdParentComposite.bottom = new FormAttachment(100, 0); + parentComposite.setLayoutData(fdParentComposite); + PropsUi.setLook(parentComposite); + + GuiCompositeWidgets compositeWidgets = + new GuiCompositeWidgets(HopGui.getInstance().getVariables()); + compositeWidgets.createCompositeWidgets( + this, null, parentComposite, WELCOME_MARKETPLACE_PARENT_ID, null); + } + + @GuiWidgetElement( + id = "WelcomeMarketplace.10010.overview", + parentId = WELCOME_MARKETPLACE_PARENT_ID, + type = GuiElementType.LINK, + label = + "Large and lesser-used plugins are no longer bundled in the default hop-client download " + + "(Apache packaging size limit).\n\n" + + "Install them from the marketplace: plugins are published as Maven zips on the " + + "Apache Repository (repository.apache.org), with Maven Central as fallback.\n\n" + + "See the " + + WEB_NAME_MARKETPLACE_DOCS + + " for CLI and GUI details.") + public void overviewLink(Event event) { + handleWebLinkEvent(event); + } + + @GuiWidgetElement( + id = "WelcomeMarketplace.10020.open-dialog", + parentId = WELCOME_MARKETPLACE_PARENT_ID, + type = GuiElementType.LINK, + label = + "\nOpen the marketplace from Tools → Marketplace…, the main toolbar icon after Save As…, " + + "or click " + + LINK_OPEN_MARKETPLACE + + ".") + public void openMarketplaceLink(Event event) { + openMarketplace(event); + } + + @GuiWidgetElement( + id = "WelcomeMarketplace.10030.install-all", + parentId = WELCOME_MARKETPLACE_PARENT_ID, + type = GuiElementType.LINK, + label = + "\nTo install all optional plugins that used to ship in the fat client:\n\n" + + " • GUI: open the marketplace, select full-client-env.yaml as the environment file " + + "(pre-filled when present at the install root), click Apply, then restart Hop.\n" + + " • CLI (from the Hop install directory):\n\n" + + " ./hop marketplace apply -f full-client-env.yaml\n\n" + + "Do not use prune unless you also want other marketplace installs removed.\n\n" + + "Click " + + LINK_OPEN_MARKETPLACE + + " to open the dialog and Apply now.") + public void installAllLink(Event event) { + openMarketplace(event); + } + + private void openMarketplace(Event event) { + try { + if (WelcomeMarketplace.LINK_OPEN_MARKETPLACE.equals(event.text)) { + MarketplaceGuiPlugin.getInstance().menuToolsMarketplace(); + } + } catch (Exception e) { + new ErrorDialog( + HopGui.getInstance().getShell(), "Error", "Unable to open the marketplace dialog", e); + } + } + + private void handleWebLinkEvent(Event event) { + try { + if (WelcomeMarketplace.WEB_NAME_MARKETPLACE_DOCS.equals(event.text)) { + EnvironmentUtils.getInstance().openUrl(WelcomeMarketplace.WEB_LINK_MARKETPLACE_DOCS); + } + } catch (Exception e) { + new ErrorDialog( + HopGui.getInstance().getShell(), + "Error", + "Error opening link to " + WelcomeMarketplace.WEB_LINK_MARKETPLACE_DOCS, + e); + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/HopHome.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/HopHome.java new file mode 100644 index 00000000000..9cb5591f8fa --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/HopHome.java @@ -0,0 +1,71 @@ +/* + * 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.marketplace.install; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import org.apache.hop.core.exception.HopException; + +/** + * Resolves the Hop client install directory (folder containing {@code plugins/}). + * + *

Hop launchers {@code cd} to the install root before starting the JVM, so the process working + * directory is the install. Marketplace unpacks plugins into that tree only — the same + * {@code plugins/} folder Hop already scans. + */ +public final class HopHome { + + private HopHome() {} + + public static Path resolve() throws HopException { + List tried = new ArrayList<>(); + for (Path candidate : candidates()) { + Path abs = candidate.toAbsolutePath().normalize(); + tried.add(abs.toString()); + if (isHopHome(abs)) { + return abs; + } + } + throw new HopException( + "Cannot determine Hop installation directory (need a folder containing plugins/)." + + " Run the hop launcher from the client install (./hop …)." + + " Tried: " + + String.join(", ", tried)); + } + + private static Set candidates() { + Set paths = new LinkedHashSet<>(); + Path cwd = Paths.get(System.getProperty("user.dir", ".")); + paths.add(cwd); + Path parent = cwd.toAbsolutePath().normalize().getParent(); + if (parent != null) { + paths.add(parent); + } + return paths; + } + + /** True if path is a directory that looks like a Hop install (has {@code plugins/}). */ + static boolean isHopHome(Path path) { + return path != null && Files.isDirectory(path) && Files.isDirectory(path.resolve("plugins")); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/InstallReceipt.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/InstallReceipt.java new file mode 100644 index 00000000000..12edeb60632 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/InstallReceipt.java @@ -0,0 +1,45 @@ +/* + * 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.marketplace.install; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class InstallReceipt { + private String groupId; + private String artifactId; + private String version; + private String installedAt; + + /** Repository id that supplied the zip (if known). */ + private String repositoryId; + + /** Repository base URL used for the download. */ + private String repositoryUrl; + + private List paths = new ArrayList<>(); + private boolean pendingActivation; + + public InstallReceipt() { + // Jackson + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java new file mode 100644 index 00000000000..e8410b1c7d7 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginInstaller.java @@ -0,0 +1,346 @@ +/* + * 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.marketplace.install; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.resolve.MavenCoordinates; +import org.apache.hop.marketplace.resolve.MavenRepositoryClient; + +/** + * Downloads a plugin zip, stages it under {@code plugins/.staging/}, writes a receipt, and + * activates into the live plugins tree. Uses stage-then-activate (restart-friendly) rather than + * hot-reload. + */ +public class PluginInstaller { + + public static final String STAGING_DIR = "plugins/.staging"; + public static final String RECEIPTS_DIR = "plugins/.marketplace"; + + private final ILogChannel log; + private final Path hopHome; + private final MarketplaceConfig config; + private final MavenRepositoryClient client; + + public PluginInstaller(ILogChannel log, Path hopHome, MarketplaceConfig config) { + this.log = log; + this.hopHome = hopHome; + this.config = config; + this.client = new MavenRepositoryClient(log); + } + + PluginInstaller( + ILogChannel log, Path hopHome, MarketplaceConfig config, MavenRepositoryClient client) { + this.log = log; + this.hopHome = hopHome; + this.config = config; + this.client = client; + } + + /** + * Download, stage, and activate a plugin. Returns the receipt after activation. + * + * @param activateImmediately if true, move staged files into place now (CLI default). GUI may + * stage and ask for restart later; for CLI we activate immediately then ask the user to + * restart so classloaders pick up the new jars. + */ + public InstallReceipt install(MavenCoordinates coordinates, boolean activateImmediately) + throws HopException { + return install(coordinates, activateImmediately, null, null); + } + + /** + * @param forceRepoId when non-blank, only that repository is used (no fallback chain) + */ + public InstallReceipt install( + MavenCoordinates coordinates, boolean activateImmediately, String forceRepoId) + throws HopException { + return install(coordinates, activateImmediately, forceRepoId, null); + } + + /** + * @param forceRepoId when non-blank, only that repository is used (no fallback chain) + * @param preferredRepoId when non-blank and force is blank, try this repository first then the + * normal fallback chain (used when discovery knows the source repo) + */ + public InstallReceipt install( + MavenCoordinates coordinates, + boolean activateImmediately, + String forceRepoId, + String preferredRepoId) + throws HopException { + Path downloadDir = hopHome.resolve(STAGING_DIR).resolve(".download"); + Path zipFile = + downloadDir.resolve(coordinates.artifactId() + "-" + coordinates.version() + ".zip"); + try { + List repos = resolveRepositories(forceRepoId, preferredRepoId); + MarketplaceRepository used = null; + List errors = new ArrayList<>(); + for (MarketplaceRepository repo : repos) { + try { + client.downloadZip(repo, coordinates, zipFile); + used = repo; + break; + } catch (HopException e) { + errors.add( + repo.getId() + + " @ " + + repo.normalizedUrl() + + " → " + + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage())); + log.logBasic("Repository attempt failed: " + errors.get(errors.size() - 1)); + } + } + if (used == null) { + throw new HopException( + "Could not download " + + coordinates.gav() + + " from any configured repository:\n - " + + String.join("\n - ", errors)); + } + + Path stageRoot = hopHome.resolve(STAGING_DIR).resolve(coordinates.artifactId()); + deleteRecursive(stageRoot); + Files.createDirectories(stageRoot); + List relativePaths = unzipAllowedEntries(zipFile, stageRoot); + + InstallReceipt receipt = new InstallReceipt(); + receipt.setGroupId(coordinates.groupId()); + receipt.setArtifactId(coordinates.artifactId()); + receipt.setVersion(coordinates.version()); + receipt.setInstalledAt(Instant.now().toString()); + receipt.setRepositoryId(used.getId()); + receipt.setRepositoryUrl(used.normalizedUrl()); + receipt.setPaths(relativePaths); + receipt.setPendingActivation(!activateImmediately); + writeReceipt(receipt); + + if (activateImmediately) { + activateStaged(coordinates.artifactId(), relativePaths); + receipt.setPendingActivation(false); + writeReceipt(receipt); + deleteRecursive(stageRoot); + } + + Files.deleteIfExists(zipFile); + log.logBasic( + "Installed " + + coordinates.gav() + + (activateImmediately + ? ". Restart Hop to load the plugin." + : ". Staged for activation on next restart.")); + return receipt; + } catch (IOException e) { + throw new HopException("Failed to install plugin " + coordinates.gav(), e); + } + } + + private List resolveRepositories( + String forceRepoId, String preferredRepoId) throws HopException { + if (StringUtils.isNotBlank(forceRepoId)) { + MarketplaceRepository forced = config.findRepository(forceRepoId); + if (forced == null) { + throw new HopException("Unknown marketplace repository id: " + forceRepoId); + } + if (!forced.isEnabled()) { + throw new HopException("Marketplace repository is disabled: " + forceRepoId); + } + return List.of(forced); + } + List ordered = new ArrayList<>(config.orderedRepositories()); + if (StringUtils.isNotBlank(preferredRepoId)) { + MarketplaceRepository preferred = null; + for (int i = 0; i < ordered.size(); i++) { + MarketplaceRepository r = ordered.get(i); + if (r != null && preferredRepoId.equals(r.getId())) { + preferred = ordered.remove(i); + break; + } + } + if (preferred != null) { + ordered.add(0, preferred); + } + } + return ordered; + } + + /** Activate all staged plugins (startup hook). */ + public void activateAllPending() throws HopException { + Path staging = hopHome.resolve(STAGING_DIR); + if (!Files.isDirectory(staging)) { + return; + } + try { + try (var stream = Files.list(staging)) { + for (Path staged : stream.filter(Files::isDirectory).toList()) { + if (staged.getFileName().toString().startsWith(".")) { + continue; + } + List paths = collectRelativePaths(staged); + activateStaged(staged.getFileName().toString(), paths); + deleteRecursive(staged); + log.logBasic("Activated staged plugin: " + staged.getFileName()); + } + } + } catch (IOException e) { + throw new HopException("Failed to activate staged plugins", e); + } + } + + private void activateStaged(String artifactId, List relativePaths) throws HopException { + Path stageRoot = hopHome.resolve(STAGING_DIR).resolve(artifactId); + try { + for (String relative : relativePaths) { + // Never overwrite core beam libraries from a marketplace zip + if (isProtectedPath(relative)) { + log.logBasic("Skipping protected path from marketplace package: " + relative); + continue; + } + Path from = stageRoot.resolve(relative); + Path to = hopHome.resolve(relative); + if (Files.isDirectory(from)) { + Files.createDirectories(to); + } else if (Files.isRegularFile(from)) { + Files.createDirectories(to.getParent()); + Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING); + } + } + } catch (IOException e) { + throw new HopException("Failed to activate staged plugin " + artifactId, e); + } + } + + /** + * Core install paths marketplace zips must not overwrite. Beam SDKs live under the Beam plugin + * ({@code plugins/engines/beam/lib-beam}) and are installed/uninstalled with that plugin. + */ + static boolean isProtectedPath(String relative) { + String normalized = relative.replace('\\', '/'); + return normalized.equals("lib/core") || normalized.startsWith("lib/core/"); + } + + private List unzipAllowedEntries(Path zipFile, Path stageRoot) + throws IOException, HopException { + List paths = new ArrayList<>(); + try (ZipFile zip = new ZipFile(zipFile.toFile())) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.isBlank()) { + continue; + } + // Normalize and reject path traversal + Path resolved = stageRoot.resolve(name).normalize(); + if (!resolved.startsWith(stageRoot)) { + throw new HopException("Zip entry escapes target directory: " + name); + } + if (entry.isDirectory()) { + Files.createDirectories(resolved); + } else { + Files.createDirectories(resolved.getParent()); + try (InputStream in = zip.getInputStream(entry)) { + Files.copy(in, resolved, StandardCopyOption.REPLACE_EXISTING); + } + } + String relative = stageRoot.relativize(resolved).toString().replace('\\', '/'); + if (!relative.isBlank()) { + paths.add(relative); + } + } + } + return paths; + } + + private void writeReceipt(InstallReceipt receipt) throws HopException { + try { + Path dir = hopHome.resolve(RECEIPTS_DIR); + Files.createDirectories(dir); + Path file = dir.resolve(receipt.getArtifactId() + ".json"); + HopJson.newMapper().writerWithDefaultPrettyPrinter().writeValue(file.toFile(), receipt); + } catch (IOException e) { + throw new HopException("Unable to write install receipt for " + receipt.getArtifactId(), e); + } + } + + public static InstallReceipt readReceipt(Path hopHome, String artifactId) throws HopException { + Path file = hopHome.resolve(RECEIPTS_DIR).resolve(artifactId + ".json"); + if (!Files.isRegularFile(file)) { + return null; + } + try { + return HopJson.newMapper().readValue(file.toFile(), InstallReceipt.class); + } catch (IOException e) { + throw new HopException("Unable to read install receipt for " + artifactId, e); + } + } + + private static List collectRelativePaths(Path stageRoot) throws IOException { + List paths = new ArrayList<>(); + Files.walkFileTree( + stageRoot, + new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + paths.add(stageRoot.relativize(file).toString().replace('\\', '/')); + return FileVisitResult.CONTINUE; + } + }); + return paths; + } + + static void deleteRecursive(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + Files.walkFileTree( + root, + new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginUninstaller.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginUninstaller.java new file mode 100644 index 00000000000..e4b1ddb8c41 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/install/PluginUninstaller.java @@ -0,0 +1,108 @@ +/* + * 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.marketplace.install; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; + +/** Removes a marketplace-installed plugin using its receipt. Never touches core-protected paths. */ +public class PluginUninstaller { + + private final ILogChannel log; + private final Path hopHome; + + public PluginUninstaller(ILogChannel log, Path hopHome) { + this.log = log; + this.hopHome = hopHome; + } + + public void uninstall(String artifactId) throws HopException { + InstallReceipt receipt = PluginInstaller.readReceipt(hopHome, artifactId); + if (receipt == null) { + throw new HopException( + "No marketplace install receipt for '" + + artifactId + + "'. Only plugins installed via the marketplace can be uninstalled this way."); + } + + // Delete files deepest-first; skip protected paths (e.g. lib/core) + Set dirs = new HashSet<>(); + for (String relative : receipt.getPaths()) { + if (PluginInstaller.isProtectedPath(relative)) { + log.logBasic("Leaving protected path in place: " + relative); + continue; + } + Path path = hopHome.resolve(relative); + try { + if (Files.isRegularFile(path)) { + Files.deleteIfExists(path); + Path parent = path.getParent(); + if (parent != null) { + dirs.add(parent); + } + } else if (Files.isDirectory(path)) { + dirs.add(path); + } + } catch (IOException e) { + throw new HopException("Failed to remove " + path, e); + } + } + + // Remove empty plugin directories under plugins/ + dirs.stream() + .sorted(Comparator.comparingInt(p -> -p.getNameCount())) + .forEach( + dir -> { + try { + if (Files.isDirectory(dir) && isEmpty(dir) && isUnderPlugins(dir)) { + Files.deleteIfExists(dir); + } + } catch (IOException e) { + log.logError("Could not remove directory " + dir, e); + } + }); + + Path receiptFile = hopHome.resolve(PluginInstaller.RECEIPTS_DIR).resolve(artifactId + ".json"); + try { + Files.deleteIfExists(receiptFile); + } catch (IOException e) { + throw new HopException("Failed to remove receipt for " + artifactId, e); + } + log.logBasic( + "Uninstalled " + + artifactId + + ". Restart Hop so the plugin registry drops the removed classes."); + } + + private boolean isUnderPlugins(Path dir) { + Path plugins = hopHome.resolve("plugins").toAbsolutePath().normalize(); + return dir.toAbsolutePath().normalize().startsWith(plugins); + } + + private static boolean isEmpty(Path dir) throws IOException { + try (var stream = Files.list(dir)) { + return stream.findAny().isEmpty(); + } + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenCoordinates.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenCoordinates.java new file mode 100644 index 00000000000..6b663b1051b --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenCoordinates.java @@ -0,0 +1,71 @@ +/* + * 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.marketplace.resolve; + +import java.util.Objects; +import org.apache.hop.core.exception.HopException; + +/** Simple GAV for Hop plugin zips. */ +public record MavenCoordinates(String groupId, String artifactId, String version) { + + public MavenCoordinates { + Objects.requireNonNull(groupId, "groupId"); + Objects.requireNonNull(artifactId, "artifactId"); + Objects.requireNonNull(version, "version"); + } + + /** + * Parse {@code artifactId}, {@code artifactId:version}, or {@code groupId:artifactId:version}. + */ + public static MavenCoordinates parse(String spec, String defaultGroupId, String defaultVersion) + throws HopException { + if (spec == null || spec.isBlank()) { + throw new HopException("Plugin coordinate is empty"); + } + String[] parts = spec.trim().split(":"); + return switch (parts.length) { + case 1 -> new MavenCoordinates(defaultGroupId, parts[0], requireVersion(defaultVersion)); + case 2 -> new MavenCoordinates(defaultGroupId, parts[0], parts[1]); + case 3 -> new MavenCoordinates(parts[0], parts[1], parts[2]); + default -> + throw new HopException( + "Invalid coordinate '" + + spec + + "'. Use artifactId, artifactId:version, or groupId:artifactId:version"); + }; + } + + private static String requireVersion(String defaultVersion) throws HopException { + if (defaultVersion == null || defaultVersion.isBlank()) { + throw new HopException( + "No version specified and no default Hop version available. Use artifactId:version"); + } + return defaultVersion; + } + + /** Maven repository relative path to the zip artifact (no leading slash). */ + public String zipRepositoryPath() { + String groupPath = groupId.replace('.', '/'); + String fileName = artifactId + "-" + version + ".zip"; + return groupPath + "/" + artifactId + "/" + version + "/" + fileName; + } + + public String gav() { + return groupId + ":" + artifactId + ":" + version; + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenRepositoryClient.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenRepositoryClient.java new file mode 100644 index 00000000000..56247c27735 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/MavenRepositoryClient.java @@ -0,0 +1,303 @@ +/* + * 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.marketplace.resolve; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.marketplace.config.MarketplaceRepository; + +/** Downloads Maven layout artifacts over HTTP(S), with optional Basic authentication. */ +public class MavenRepositoryClient { + + private static final Pattern SNAPSHOT_ZIP_VALUE = + Pattern.compile( + "\\s*zip\\s*([^<]+)", + Pattern.DOTALL); + private static final Pattern SNAPSHOT_TIMESTAMP = + Pattern.compile( + "\\s*([^<]+)\\s*([^<]+)", + Pattern.DOTALL); + + private final HttpClient httpClient; + private final ILogChannel log; + + public MavenRepositoryClient(ILogChannel log) { + this.log = log; + this.httpClient = + HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .followRedirects(HttpClient.Redirect.NORMAL) + .build(); + } + + public MavenRepositoryClient(ILogChannel log, HttpClient httpClient) { + this.log = log; + this.httpClient = httpClient; + } + + public Path downloadZip( + MarketplaceRepository repository, MavenCoordinates coordinates, Path targetFile) + throws HopException { + String base = repository.normalizedUrl(); + String relative = resolveZipRelativePath(repository, coordinates); + String url = base + relative; + return download(url, repository, coordinates.gav(), targetFile); + } + + /** + * Try each repository in order until a zip is downloaded. Aggregates per-repo errors if all fail + * (fallback chain). + */ + public Path downloadZipWithFallback( + List repositories, MavenCoordinates coordinates, Path targetFile) + throws HopException { + if (repositories == null || repositories.isEmpty()) { + throw new HopException("No marketplace repositories configured"); + } + List errors = new ArrayList<>(); + for (MarketplaceRepository repository : repositories) { + if (repository == null || !repository.isEnabled()) { + continue; + } + try { + log.logBasic( + "Trying repository '" + + repository.displayName() + + "' (" + + repository.normalizedUrl() + + ") for " + + coordinates.gav()); + return downloadZip(repository, coordinates, targetFile); + } catch (HopException e) { + String msg = + repository.getId() + + " @ " + + repository.normalizedUrl() + + " → " + + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()); + errors.add(msg); + log.logBasic("Repository attempt failed: " + msg); + } + } + throw new HopException( + "Could not download " + + coordinates.gav() + + " from any configured repository:\n - " + + String.join("\n - ", errors)); + } + + /** + * @deprecated use {@link #downloadZip(MarketplaceRepository, MavenCoordinates, Path)} + */ + public Path downloadZip(String repositoryBaseUrl, MavenCoordinates coordinates, Path targetFile) + throws HopException { + MarketplaceRepository repo = new MarketplaceRepository("adhoc", repositoryBaseUrl); + return downloadZip(repo, coordinates, targetFile); + } + + public void downloadArtifact( + MarketplaceRepository repository, String relativePath, String label, Path targetFile) + throws HopException { + String base = repository.normalizedUrl(); + String url = base + (relativePath.startsWith("/") ? relativePath.substring(1) : relativePath); + download(url, repository, label, targetFile); + } + + /** + * For release versions: {@code g/a/v/a-v.zip}. For {@code *-SNAPSHOT} (including unique + * timestamped forms like {@code 1.0.0-20260721.105615-1} from Nexus search), resolve the unique + * file name from {@code maven-metadata.xml} under the base SNAPSHOT directory. + */ + String resolveZipRelativePath(MarketplaceRepository repository, MavenCoordinates coordinates) + throws HopException { + String version = SnapshotVersions.toBaseVersion(coordinates.version()); + if (version == null || !version.endsWith("-SNAPSHOT")) { + return coordinates.zipRepositoryPath(); + } + + // When the caller already passed a unique SNAPSHOT, place that file under the base folder + // without requiring metadata (Nexus search already named the asset). + if (SnapshotVersions.isUniqueSnapshot(coordinates.version())) { + String groupPath = coordinates.groupId().replace('.', '/'); + return groupPath + + "/" + + coordinates.artifactId() + + "/" + + version + + "/" + + coordinates.artifactId() + + "-" + + coordinates.version().trim() + + ".zip"; + } + + String groupPath = coordinates.groupId().replace('.', '/'); + String metadataPath = + groupPath + "/" + coordinates.artifactId() + "/" + version + "/maven-metadata.xml"; + String metadataUrl = repository.normalizedUrl() + metadataPath; + log.logDetailed("Resolving SNAPSHOT zip via " + metadataUrl); + String metadata = getText(metadataUrl, repository); + String unique = parseSnapshotZipValue(metadata, coordinates.artifactId(), version); + if (unique == null) { + // Fall back to non-unique name (some repos allow it) + log.logBasic( + "Could not parse SNAPSHOT zip from maven-metadata.xml; trying non-unique file name"); + return groupPath + + "/" + + coordinates.artifactId() + + "/" + + version + + "/" + + coordinates.artifactId() + + "-" + + version + + ".zip"; + } + return groupPath + + "/" + + coordinates.artifactId() + + "/" + + version + + "/" + + coordinates.artifactId() + + "-" + + unique + + ".zip"; + } + + static String parseSnapshotZipValue( + String metadataXml, String artifactId, String snapshotVersion) { + if (metadataXml == null || metadataXml.isBlank()) { + return null; + } + Matcher m = SNAPSHOT_ZIP_VALUE.matcher(metadataXml); + if (m.find()) { + return m.group(1).trim(); + } + // timestamp + buildNumber → 2.19.0-20260719.204953-1 + Matcher t = SNAPSHOT_TIMESTAMP.matcher(metadataXml); + if (t.find()) { + String base = snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length()); + return base + "-" + t.group(1).trim() + "-" + t.group(2).trim(); + } + return null; + } + + private String getText(String url, MarketplaceRepository repository) throws HopException { + try { + HttpRequest.Builder builder = + HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofMinutes(2)).GET(); + applyBasicAuth(builder, repository); + HttpResponse response = + httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 401 || response.statusCode() == 403) { + throw new HopException(authFailureMessage(response.statusCode(), url, repository)); + } + if (response.statusCode() != 200) { + throw new HopException( + "HTTP " + response.statusCode() + " fetching maven-metadata from " + url); + } + return response.body(); + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new HopException("Failed to fetch " + url, e); + } + } + + private Path download(String url, MarketplaceRepository repository, String label, Path targetFile) + throws HopException { + log.logBasic("Downloading " + label + " from " + url); + try { + Files.createDirectories(targetFile.getParent()); + HttpRequest.Builder builder = + HttpRequest.newBuilder(URI.create(url)).timeout(Duration.ofMinutes(30)).GET(); + applyBasicAuth(builder, repository); + HttpResponse response = + httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() == 401 || response.statusCode() == 403) { + throw new HopException(authFailureMessage(response.statusCode(), url, repository)); + } + if (response.statusCode() != 200) { + throw new HopException( + "HTTP " + response.statusCode() + " downloading " + label + " from " + url); + } + try (InputStream in = response.body()) { + Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + log.logBasic("Downloaded " + targetFile + " (" + Files.size(targetFile) + " bytes)"); + return targetFile; + } catch (IOException | InterruptedException e) { + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + throw new HopException("Failed to download " + label + " from " + url, e); + } + } + + static void applyBasicAuth(HttpRequest.Builder builder, MarketplaceRepository repository) { + if (repository == null || !repository.hasCredentials()) { + return; + } + String user = repository.effectiveUsername(); + String pass = repository.effectivePassword(); + if (StringUtils.isAnyBlank(user, pass)) { + return; + } + String token = + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8)); + builder.header("Authorization", "Basic " + token); + } + + private static String authFailureMessage( + int status, String url, MarketplaceRepository repository) { + StringBuilder sb = new StringBuilder(); + sb.append("HTTP ").append(status).append(" from ").append(url).append(". "); + if (repository != null && repository.hasCredentials()) { + sb.append("Basic auth was sent as user '") + .append(repository.effectiveUsername()) + .append("'. Wrong credentials make Nexus reject the request even when anonymous read") + .append(" would work. Clear username/password in hop-config marketplace.repositories") + .append(" and unset HOP_MARKETPLACE_USERNAME / HOP_MARKETPLACE_PASSWORD, or fix the") + .append(" password."); + } else { + sb.append("No credentials were sent (anonymous). Enable anonymous read on the repository") + .append(" (docker/marketplace-nexus/start.sh does this), or set") + .append(" HOP_MARKETPLACE_USERNAME / HOP_MARKETPLACE_PASSWORD for a private repo."); + } + return sb.toString(); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/SnapshotVersions.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/SnapshotVersions.java new file mode 100644 index 00000000000..65a916695b3 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/resolve/SnapshotVersions.java @@ -0,0 +1,107 @@ +/* + * 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.marketplace.resolve; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +/** + * Maven SNAPSHOT helpers. Nexus search often reports the unique version ({@code + * 1.0.0-20260721.105615-1}) while repository layout stores files under the base folder ({@code + * 1.0.0-SNAPSHOT/}). + */ +public final class SnapshotVersions { + + /** Unique timestamped SNAPSHOT: {@code base-yyyyMMdd.HHmmss-buildNumber}. */ + private static final Pattern UNIQUE_SNAPSHOT = Pattern.compile("^(.*)-(\\d{8}\\.\\d{6})-(\\d+)$"); + + private SnapshotVersions() {} + + /** True for {@code *-SNAPSHOT} or unique timestamped SNAPSHOT versions. */ + public static boolean isSnapshot(String version) { + if (StringUtils.isBlank(version)) { + return false; + } + return version.endsWith("-SNAPSHOT") || isUniqueSnapshot(version); + } + + /** True for {@code 1.0.0-20260721.105615-1} style unique versions. */ + public static boolean isUniqueSnapshot(String version) { + return StringUtils.isNotBlank(version) && UNIQUE_SNAPSHOT.matcher(version.trim()).matches(); + } + + /** + * Map unique SNAPSHOT versions to base {@code *-SNAPSHOT} used in Maven repository directories. + * Leaves release and already-base SNAPSHOT versions unchanged. + */ + public static String toBaseVersion(String version) { + if (StringUtils.isBlank(version)) { + return version; + } + String v = version.trim(); + if (v.endsWith("-SNAPSHOT")) { + return v; + } + Matcher m = UNIQUE_SNAPSHOT.matcher(v); + if (m.matches()) { + return m.group(1) + "-SNAPSHOT"; + } + return v; + } + + /** + * Prefer the version folder from a Maven asset path (e.g. {@code /g/a/0.4.0-SNAPSHOT/a-....zip}), + * else {@link #toBaseVersion(String)}. + */ + public static String toBaseVersion(String version, String mavenAssetPath) { + String fromPath = versionFolderFromPath(mavenAssetPath); + if (StringUtils.isNotBlank(fromPath)) { + return fromPath; + } + return toBaseVersion(version); + } + + /** + * Extract the version directory from a repository-relative path such as {@code + * org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/hop-datavault-….zip}. + */ + public static String versionFolderFromPath(String path) { + if (StringUtils.isBlank(path)) { + return null; + } + String p = path.trim(); + while (p.startsWith("/")) { + p = p.substring(1); + } + while (p.endsWith("/")) { + p = p.substring(0, p.length() - 1); + } + String[] parts = p.split("/"); + if (parts.length < 2) { + return null; + } + // …/artifactId/version/filename + return parts[parts.length - 2]; + } + + public static boolean containsSnapshotKeyword(String version) { + return version != null && version.toUpperCase(Locale.ROOT).contains("SNAPSHOT"); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/HopRunEnvironmentCheckExtensionPoint.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/HopRunEnvironmentCheckExtensionPoint.java new file mode 100644 index 00000000000..7d9cb5c9fac --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/HopRunEnvironmentCheckExtensionPoint.java @@ -0,0 +1,98 @@ +/* + * 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.marketplace.xp; + +import java.nio.file.Path; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.extension.ExtensionPoint; +import org.apache.hop.core.extension.IExtensionPoint; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.env.EnvironmentApplier; +import org.apache.hop.marketplace.env.EnvironmentDrift; +import org.apache.hop.marketplace.env.HopEnvironmentLoader; +import org.apache.hop.marketplace.env.HopEnvironmentSpec; +import org.apache.hop.marketplace.install.HopHome; +import org.apache.hop.run.HopRunBase; + +/** + * Optional drift check at hop-run start when a hop-env file is present and enforcement is enabled + * ({@code enforceOnRun: true} in the file, or {@code -Dhop.env.enforce=true}). + */ +@ExtensionPoint( + id = "HopRunEnvironmentCheckExtensionPoint", + description = "Validate hop-env.yaml against the local plugin install before hop-run executes", + extensionPointId = "HopRunStart") +public class HopRunEnvironmentCheckExtensionPoint implements IExtensionPoint { + + @Override + public void callExtensionPoint(ILogChannel log, IVariables variables, HopRunBase hopRun) + throws HopException { + Path hopHome; + try { + hopHome = HopHome.resolve(); + } catch (HopException e) { + // hop-run from unusual cwd: skip silently + return; + } + + String explicit = variables != null ? variables.getVariable("HOP_ENV_FILE") : null; + if (StringUtils.isBlank(explicit)) { + explicit = System.getProperty("hop.env.file"); + } + Path envFile = EnvironmentApplier.resolveEnvironmentFile(hopHome, explicit); + if (envFile == null) { + return; + } + + HopEnvironmentSpec env = HopEnvironmentLoader.load(envFile); + boolean enforce = + env.isEnforceOnRun() + || "true".equalsIgnoreCase(System.getProperty("hop.env.enforce", "false")) + || "Y".equalsIgnoreCase(System.getProperty("hop.env.enforce", "N")); + if (!enforce) { + log.logDetailed( + "Found environment file " + + envFile + + " (enforceOnRun=false); skipping drift check. Use hop marketplace validate -f " + + envFile.getFileName()); + return; + } + + EnvironmentApplier applier = new EnvironmentApplier(log, hopHome, MarketplaceConfig.load()); + EnvironmentDrift drift = applier.validate(env); + // For enforceOnRun we care about missing plugins/deps and version mismatches, not extras + boolean hard = + !drift.getMissingPlugins().isEmpty() + || !drift.getVersionMismatches().isEmpty() + || !drift.getMissingDependencies().isEmpty(); + if (hard) { + throw new HopException( + "FATAL: Environment drift detected against " + + envFile + + ":\n" + + drift.formatReport() + + "Run 'hop marketplace apply -f " + + envFile + + "' to fix your environment."); + } + log.logBasic("Environment file " + envFile + " matches local install."); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/LifecycleEnvironmentDialogTabsExtensionPoint.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/LifecycleEnvironmentDialogTabsExtensionPoint.java new file mode 100644 index 00000000000..95590eea6a6 --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/LifecycleEnvironmentDialogTabsExtensionPoint.java @@ -0,0 +1,252 @@ +/* + * 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.marketplace.xp; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.AttributesContext; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.extension.ExtensionPoint; +import org.apache.hop.core.extension.IExtensionPoint; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.marketplace.env.MarketplaceAttributes; +import org.apache.hop.marketplace.gui.HopEnvironmentDialog; +import org.apache.hop.marketplace.gui.MarketplaceGuiPlugin; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.AttributesDialogExtension; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.gui.GuiResource; +import org.eclipse.swt.SWT; +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; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** + * Contributes a Marketplace / Plugins tab on the lifecycle environment dialog. Settings are stored + * under {@link MarketplaceAttributes#GROUP} on the shared {@link AttributesContext}. + */ +@ExtensionPoint( + id = "MarketplaceLifecycleEnvironmentDialogTabs", + description = "Add marketplace plugin policy tab to the lifecycle environment dialog", + extensionPointId = "HopGuiLifecycleEnvironmentDialogTabs") +public class LifecycleEnvironmentDialogTabsExtensionPoint + implements IExtensionPoint { + + private static final Class PKG = MarketplaceGuiPlugin.class; + + private Text wEnvFile; + private Combo wOnEnable; + private Button wStrict; + private Button wAutoApply; + + @Override + public void callExtensionPoint( + ILogChannel log, IVariables variables, AttributesDialogExtension extension) + throws HopException { + if (extension == null || extension.getTabFolder() == null) { + return; + } + + PropsUi props = PropsUi.getInstance(); + CTabFolder folder = extension.getTabFolder(); + int margin = PropsUi.getMargin(); + int middle = props.getMiddlePct(); + + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Tab.Plugins")); + tab.setImage(GuiResource.getInstance().getImagePlugin()); + 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); + + Label wlHelp = new Label(comp, SWT.LEFT | SWT.WRAP); + PropsUi.setLook(wlHelp); + wlHelp.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Help")); + FormData fdlHelp = new FormData(); + fdlHelp.left = new FormAttachment(0, 0); + fdlHelp.top = new FormAttachment(0, 0); + fdlHelp.right = new FormAttachment(100, 0); + wlHelp.setLayoutData(fdlHelp); + + Label wlEnvFile = new Label(comp, SWT.RIGHT); + PropsUi.setLook(wlEnvFile); + wlEnvFile.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.EnvFile.Label")); + FormData fdlEnv = new FormData(); + fdlEnv.left = new FormAttachment(0, 0); + fdlEnv.top = new FormAttachment(wlHelp, margin * 2); + fdlEnv.right = new FormAttachment(middle, -margin); + wlEnvFile.setLayoutData(fdlEnv); + + Button wEditEnv = new Button(comp, SWT.PUSH); + wEditEnv.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Button.EditEnv")); + FormData fdEdit = new FormData(); + fdEdit.right = new FormAttachment(100, 0); + fdEdit.top = new FormAttachment(wlEnvFile, 0, SWT.CENTER); + wEditEnv.setLayoutData(fdEdit); + wEditEnv.addListener( + SWT.Selection, + e -> { + Path initial = null; + String text = wEnvFile.getText(); + if (StringUtils.isNotBlank(text)) { + String resolved = variables != null ? variables.resolve(text.trim()) : text.trim(); + Path candidate = Path.of(resolved).toAbsolutePath().normalize(); + if (Files.isRegularFile(candidate)) { + initial = candidate; + } + } + Path saved = new HopEnvironmentDialog(extension.getShell(), initial).open(); + if (saved != null) { + wEnvFile.setText(saved.toString()); + } + }); + + Button wBrowse = new Button(comp, SWT.PUSH); + wBrowse.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Button.Browse")); + FormData fdBrowse = new FormData(); + fdBrowse.right = new FormAttachment(wEditEnv, -margin); + fdBrowse.top = new FormAttachment(wlEnvFile, 0, SWT.CENTER); + wBrowse.setLayoutData(fdBrowse); + wBrowse.addListener( + SWT.Selection, + e -> { + String path = + BaseDialog.presentFileDialog( + false, + extension.getShell(), + new String[] {"*.yaml;*.yml;*.json", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.Env"), + BaseMessages.getString(PKG, "MarketplaceDialog.EnvFile.Filter.All") + }, + false); + if (StringUtils.isNotBlank(path)) { + wEnvFile.setText(path); + } + }); + + wEnvFile = new Text(comp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wEnvFile); + FormData fdEnv = new FormData(); + fdEnv.left = new FormAttachment(middle, 0); + fdEnv.top = new FormAttachment(wlEnvFile, 0, SWT.CENTER); + fdEnv.right = new FormAttachment(wBrowse, -margin); + wEnvFile.setLayoutData(fdEnv); + + Label wlOnEnable = new Label(comp, SWT.RIGHT); + PropsUi.setLook(wlOnEnable); + wlOnEnable.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.OnEnable.Label")); + FormData fdlOn = new FormData(); + fdlOn.left = new FormAttachment(0, 0); + fdlOn.top = new FormAttachment(wEnvFile, margin * 2); + fdlOn.right = new FormAttachment(middle, -margin); + wlOnEnable.setLayoutData(fdlOn); + + wOnEnable = new Combo(comp, SWT.READ_ONLY | SWT.BORDER); + PropsUi.setLook(wOnEnable); + wOnEnable.setItems( + MarketplaceAttributes.ON_ENABLE_OFF, + MarketplaceAttributes.ON_ENABLE_WARN, + MarketplaceAttributes.ON_ENABLE_ENFORCE); + FormData fdOn = new FormData(); + fdOn.left = new FormAttachment(middle, 0); + fdOn.top = new FormAttachment(wlOnEnable, 0, SWT.CENTER); + fdOn.right = new FormAttachment(100, 0); + wOnEnable.setLayoutData(fdOn); + wOnEnable.setToolTipText( + BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.OnEnable.Tooltip")); + + wStrict = new Button(comp, SWT.CHECK); + PropsUi.setLook(wStrict); + wStrict.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Strict.Label")); + wStrict.setToolTipText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.Strict.Tooltip")); + FormData fdStrict = new FormData(); + fdStrict.left = new FormAttachment(middle, 0); + fdStrict.top = new FormAttachment(wOnEnable, margin * 2); + wStrict.setLayoutData(fdStrict); + + wAutoApply = new Button(comp, SWT.CHECK); + PropsUi.setLook(wAutoApply); + wAutoApply.setText(BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.AutoApply.Label")); + wAutoApply.setToolTipText( + BaseMessages.getString(PKG, "MarketplaceLifecycleEnv.AutoApply.Tooltip")); + FormData fdAuto = new FormData(); + fdAuto.left = new FormAttachment(middle, 0); + fdAuto.top = new FormAttachment(wStrict, margin); + wAutoApply.setLayoutData(fdAuto); + + extension.addLoadCallback(this::loadFromContext); + extension.addSaveCallback(this::saveToContext); + } + + private void loadFromContext(AttributesContext context) { + if (wEnvFile == null || wEnvFile.isDisposed()) { + return; + } + wEnvFile.setText(Const.NVL(MarketplaceAttributes.envFile(context), "")); + // Explicit attribute, or purpose-based default when unset (not written until save). + wOnEnable.setText(MarketplaceAttributes.resolveOnEnable(context, context.getPurpose())); + wStrict.setSelection(MarketplaceAttributes.isStrict(context)); + wAutoApply.setSelection(MarketplaceAttributes.isAutoApply(context)); + } + + private void saveToContext(AttributesContext context) { + if (wEnvFile == null || wEnvFile.isDisposed()) { + return; + } + String envFile = wEnvFile.getText(); + if (StringUtils.isBlank(envFile)) { + // clear key if empty + if (context.getAttributes(MarketplaceAttributes.GROUP) != null) { + context + .getAttributes(MarketplaceAttributes.GROUP) + .remove(MarketplaceAttributes.KEY_ENV_FILE); + } + } else { + context.setAttribute( + MarketplaceAttributes.GROUP, MarketplaceAttributes.KEY_ENV_FILE, envFile.trim()); + } + context.setAttribute( + MarketplaceAttributes.GROUP, + MarketplaceAttributes.KEY_ON_ENABLE, + Const.NVL(wOnEnable.getText(), MarketplaceAttributes.ON_ENABLE_OFF)); + context.setAttribute( + MarketplaceAttributes.GROUP, + MarketplaceAttributes.KEY_STRICT, + Boolean.toString(wStrict.getSelection())); + context.setAttribute( + MarketplaceAttributes.GROUP, + MarketplaceAttributes.KEY_AUTO_APPLY, + Boolean.toString(wAutoApply.getSelection())); + } +} diff --git a/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/ProjectEnvironmentAfterEnabledExtensionPoint.java b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/ProjectEnvironmentAfterEnabledExtensionPoint.java new file mode 100644 index 00000000000..f12002b983b --- /dev/null +++ b/plugins/misc/marketplace/src/main/java/org/apache/hop/marketplace/xp/ProjectEnvironmentAfterEnabledExtensionPoint.java @@ -0,0 +1,209 @@ +/* + * 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.marketplace.xp; + +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.AttributesContext; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.extension.ExtensionPoint; +import org.apache.hop.core.extension.IExtensionPoint; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.env.EnvironmentApplier; +import org.apache.hop.marketplace.env.EnvironmentDrift; +import org.apache.hop.marketplace.env.HopEnvironmentLoader; +import org.apache.hop.marketplace.env.HopEnvironmentSpec; +import org.apache.hop.marketplace.env.MarketplaceAttributes; +import org.apache.hop.marketplace.install.HopHome; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; + +/** + * When a project lifecycle environment is enabled, optionally validate (or apply) the marketplace + * hop-env file against the local install. Settings come from {@link AttributesContext} group {@link + * MarketplaceAttributes#GROUP} — no dependency on the Projects plugin classes. + */ +@ExtensionPoint( + id = "MarketplaceProjectEnvironmentAfterEnabled", + description = + "Validate hop-env against the local install when a lifecycle environment is enabled", + extensionPointId = "HopProjectEnvironmentAfterEnabled") +public class ProjectEnvironmentAfterEnabledExtensionPoint + implements IExtensionPoint { + + @Override + public void callExtensionPoint(ILogChannel log, IVariables variables, AttributesContext context) + throws HopException { + if (context == null) { + return; + } + + String onEnable = MarketplaceAttributes.resolveOnEnable(context, context.getPurpose()); + if (MarketplaceAttributes.ON_ENABLE_OFF.equals(onEnable)) { + log.logDetailed( + "Marketplace environment check skipped (onEnable=off) for environment '" + + Const.NVL(context.getEnvironmentName(), "") + + "'"); + return; + } + + Path hopHome; + try { + hopHome = HopHome.resolve(); + } catch (HopException e) { + log.logDetailed("Marketplace environment check skipped: Hop home not resolved"); + return; + } + + Path envFile = resolveEnvFile(context, variables, hopHome); + if (envFile == null) { + String msg = + "Marketplace environment file not found for environment '" + + Const.NVL(context.getEnvironmentName(), "") + + "'. Set marketplace attribute envFile or place hop-env.yaml under the project home."; + if (MarketplaceAttributes.ON_ENABLE_ENFORCE.equals(onEnable)) { + throw new HopException(msg); + } + log.logBasic(msg); + return; + } + + try { + MarketplaceConfig config = MarketplaceConfig.load(); + HopEnvironmentSpec env = HopEnvironmentLoader.load(envFile); + EnvironmentApplier applier = new EnvironmentApplier(log, hopHome, config); + EnvironmentDrift drift = applier.validate(env); + + if (MarketplaceAttributes.isStrict(context)) { + populateExtraPlugins(hopHome, env, drift); + } + + boolean hard = + !drift.getMissingPlugins().isEmpty() + || !drift.getVersionMismatches().isEmpty() + || !drift.getMissingDependencies().isEmpty() + || (MarketplaceAttributes.isStrict(context) + && !drift.getExtraMarketplacePlugins().isEmpty()); + + if (!hard) { + log.logBasic("Marketplace environment file " + envFile + " matches local install."); + return; + } + + String report = + "Environment drift for '" + + Const.NVL(context.getEnvironmentName(), "") + + "' against " + + envFile + + ":\n" + + drift.formatReport() + + "Run 'hop marketplace apply -f " + + envFile + + "' to fix your environment."; + + if (MarketplaceAttributes.isAutoApply(context) && config.isEnabled()) { + log.logBasic("Auto-applying marketplace environment file " + envFile); + applier.apply(env, false); + return; + } + + if (MarketplaceAttributes.ON_ENABLE_ENFORCE.equals(onEnable)) { + throw new HopException("FATAL: " + report); + } + + // warn + log.logError(report); + if ("GUI".equalsIgnoreCase(Const.getHopPlatformRuntime())) { + try { + MessageBox box = + new MessageBox(HopGui.getInstance().getShell(), SWT.OK | SWT.ICON_WARNING); + box.setText("Marketplace environment drift"); + box.setMessage(report); + box.open(); + } catch (Exception e) { + // headless or shell unavailable + } + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to validate marketplace environment file " + envFile, e); + } + } + + private static Path resolveEnvFile( + AttributesContext context, IVariables variables, Path hopHome) { + String explicit = MarketplaceAttributes.envFile(context); + if (StringUtils.isNotBlank(explicit)) { + String resolved = variables != null ? variables.resolve(explicit.trim()) : explicit.trim(); + Path path = Path.of(resolved).toAbsolutePath().normalize(); + if (Files.isRegularFile(path)) { + return path; + } + } + + // Project home hop-env.yaml + if (StringUtils.isNotBlank(context.getProjectHome())) { + Path candidate = Path.of(context.getProjectHome()).resolve("hop-env.yaml"); + if (Files.isRegularFile(candidate)) { + return candidate.toAbsolutePath().normalize(); + } + candidate = Path.of(context.getProjectHome()).resolve("hop-env.yml"); + if (Files.isRegularFile(candidate)) { + return candidate.toAbsolutePath().normalize(); + } + } + + String varFile = variables != null ? variables.getVariable("HOP_ENV_FILE") : null; + return EnvironmentApplier.resolveEnvironmentFile(hopHome, varFile); + } + + private static void populateExtraPlugins( + Path hopHome, HopEnvironmentSpec env, EnvironmentDrift drift) throws Exception { + Set desired = new HashSet<>(); + if (env.getPlugins() != null) { + for (HopEnvironmentSpec.PluginRef ref : env.getPlugins()) { + if (ref.getArtifactId() != null) { + desired.add(ref.getArtifactId()); + } + } + } + Path receipts = hopHome.resolve(PluginInstaller.RECEIPTS_DIR); + if (!Files.isDirectory(receipts)) { + return; + } + try (DirectoryStream stream = Files.newDirectoryStream(receipts, "*.json")) { + for (Path f : stream) { + String name = f.getFileName().toString(); + String id = name.substring(0, name.length() - ".json".length()); + if (!desired.contains(id)) { + drift.getExtraMarketplacePlugins().add(id); + } + } + } + } +} diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_de_DE.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_de_DE.properties new file mode 100644 index 00000000000..b10b9ee27b1 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_de_DE.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace-Plugins +MarketplaceLifecycleEnv.Help=Der Plugin-Satz wird in einer hop-env-Datei definiert. Diese Optionen steuern, wann Hop diese Datei für diese Lifecycle-Umgebung prüft (in der Entwicklung locker; in Produktion streng). +MarketplaceLifecycleEnv.EnvFile.Label=Umgebungsdatei +MarketplaceLifecycleEnv.Button.Browse=Durchsuchen… +MarketplaceLifecycleEnv.Button.EditEnv=Bearbeiten… +MarketplaceLifecycleEnv.OnEnable.Label=Beim Aktivieren +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: keine Prüfung; warn: Protokoll/Dialog bei Abweichung; enforce: Aktivierung bei Abweichung fehlschlagen lassen +MarketplaceLifecycleEnv.Strict.Label=Streng (fehlschlagen, wenn zusätzliche Marketplace-Plugins installiert sind) +MarketplaceLifecycleEnv.Strict.Tooltip=Beim Validieren auch Marketplace-Plugins mit Receipt, die nicht in der Env-Datei stehen, als Abweichung werten +MarketplaceLifecycleEnv.AutoApply.Label=Fehlende Plugins beim Aktivieren automatisch anwenden +MarketplaceLifecycleEnv.AutoApply.Tooltip=Wenn aktiviert, wird marketplace apply für fehlende Plugins ausgeführt (nur auf kontrollierten Systemen; standardmäßig aus) +MarketplaceDialog.Shell.Title=Plugin-Marketplace +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Umgebung +MarketplaceDialog.Tab.Repositories=Repositories +MarketplaceDialog.Repository.Label=Primäres Repository +MarketplaceDialog.Repository.SetPrimary.Error=Primäres Repository konnte nicht gesetzt werden +MarketplaceDialog.Column.Name=Name +MarketplaceDialog.Column.Artifact=Artefakt +MarketplaceDialog.Column.Category=Kategorie +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Beschreibung +MarketplaceDialog.Search.Label=Suche +MarketplaceDialog.Search.Placeholder=Mindestens 3 Zeichen zum Filtern… +MarketplaceDialog.Search.Clear=Suchfilter löschen +MarketplaceDialog.Button.Install=Installieren +MarketplaceDialog.Button.Uninstall=Deinstallieren +MarketplaceDialog.Button.Refresh=Aktualisieren +MarketplaceDialog.Button.Close=Schließen +MarketplaceDialog.Button.ManageRepos=Verwalten… +MarketplaceDialog.Button.BrowseEnv=Durchsuchen… +MarketplaceDialog.Button.EditEnv=Bearbeiten… +MarketplaceDialog.Button.EditEnv.Tooltip=Umgebungsdatei erstellen oder bearbeiten (Repositories, Plugins, Abhängigkeiten) +MarketplaceDialog.Button.ValidateEnv=Validieren +MarketplaceDialog.Button.ValidateEnv.Tooltip=Lokale Installation mit der Umgebungsdatei vergleichen, ohne zu installieren +MarketplaceDialog.Button.ApplyEnv=Anwenden +MarketplaceDialog.Button.ApplyEnv.Tooltip=In der Umgebungsdatei deklarierte Plugins und Abhängigkeiten installieren +MarketplaceDialog.EnvFile.Label=Umgebungsdatei +MarketplaceDialog.EnvFile.Prune=Extras beim Anwenden entfernen +MarketplaceDialog.EnvFile.Prune.Tooltip=Beim Anwenden Marketplace-Plugins mit Receipt deinstallieren, die nicht in der Datei stehen +MarketplaceDialog.EnvFile.Strict=Strenge Validierung +MarketplaceDialog.EnvFile.Strict.Tooltip=Beim Validieren auch fehlschlagen, wenn zusätzliche Marketplace-Plugins jenseits der Datei installiert sind +MarketplaceDialog.EnvFile.Filter.Env=Hop-Umgebungsdateien (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Alle Dateien +MarketplaceDialog.EnvFile.Missing.Header=Umgebungsdatei erforderlich +MarketplaceDialog.EnvFile.Missing.Message=Wählen Sie eine hop-env.yaml (oder full-client-env.yaml) mit Durchsuchen… oder geben Sie den Pfad ein. +MarketplaceDialog.EnvFile.NotFound.Header=Datei nicht gefunden +MarketplaceDialog.EnvFile.NotFound.Message=Umgebungsdatei existiert nicht:\n{0} +MarketplaceDialog.Status.Installed=Installiert +MarketplaceDialog.Status.Present=Auf Datenträger vorhanden +MarketplaceDialog.Status.NotInstalled=Nicht installiert +MarketplaceDialog.Status.RestartHint=Starten Sie Hop nach Installation, Anwendung oder Deinstallation neu, damit Plugins neu geladen werden. Die Installation versucht zuerst das primäre Repository, dann andere aktivierte Repositories. +MarketplaceDialog.Status.Installing=Installiere {0}... +MarketplaceDialog.Status.Validating=Umgebungsdatei wird validiert... +MarketplaceDialog.Status.Applying=Umgebungsdatei wird angewendet... +MarketplaceDialog.Status.Drift=Umgebungsabweichung erkannt — mit Anwenden beheben +MarketplaceDialog.Error.Header=Marketplace-Fehler +MarketplaceDialog.Error.ResolveHome=Hop-Installationsverzeichnis konnte nicht ermittelt werden. Starten Sie Hop vom Installationsroot mit dem Hop-Launcher. +MarketplaceDialog.Install.Done.Header=Plugin installiert +MarketplaceDialog.Install.Done.Message=Installiert: {0}.\n\nBitte starten Sie Hop neu, um das Plugin zu laden. +MarketplaceDialog.Install.Error=Installation von {0} fehlgeschlagen +MarketplaceDialog.Uninstall.Confirm.Header=Plugin deinstallieren +MarketplaceDialog.Uninstall.Confirm.Message=Über den Marketplace installiertes Plugin {0} entfernen? +MarketplaceDialog.Uninstall.NoReceipt.Header=Deinstallation nicht möglich +MarketplaceDialog.Uninstall.NoReceipt.Message=Kein Marketplace-Installationsbeleg für {0}. Hier können nur über den Marketplace installierte Plugins deinstalliert werden. +MarketplaceDialog.Uninstall.Error=Deinstallation von {0} fehlgeschlagen +MarketplaceDialog.Validate.Ok.Header=Umgebung OK +MarketplaceDialog.Validate.Ok.Message=Lokale Installation stimmt überein:\n{0} +MarketplaceDialog.Validate.Drift.Header=Umgebungsabweichung +MarketplaceDialog.Validate.Drift.Message=Die lokale Installation stimmt nicht mit der Umgebungsdatei überein: +MarketplaceDialog.Validate.Drift.Hint=Verwenden Sie Anwenden, um fehlende Plugins zu installieren (oder Versionen zu korrigieren), und starten Sie Hop neu. +MarketplaceDialog.Validate.Error=Validierung der Umgebungsdatei fehlgeschlagen +MarketplaceDialog.Apply.PruneConfirm.Header=Marketplace-Plugins bereinigen? +MarketplaceDialog.Apply.PruneConfirm.Message=Anwenden mit Bereinigung deinstalliert Marketplace-Plugins, die nicht in der Umgebungsdatei stehen.\n\nFortfahren? +MarketplaceDialog.Apply.Disabled=Marketplace ist in hop-config.json deaktiviert. +MarketplaceDialog.Apply.Done.Header=Umgebung angewendet +MarketplaceDialog.Apply.Done.Message=Angewendet:\n{0}\n\nBitte starten Sie Hop neu, damit neue Plugins geladen werden. +MarketplaceDialog.Apply.Error=Anwenden der Umgebungsdatei fehlgeschlagen +ManageRepositoriesDialog.Shell.Title=Marketplace-Repositories +ManageRepositoriesDialog.Help=Das primäre Repository wird zuerst versucht. Die anderen aktivierten Repositories sind Fallbacks.\nStandard: Apache Repository (repository.apache.org), dann Maven Central.\nEinstellungen werden in hop-config.json unter '${HOP_CONFIG_FOLDER}' gespeichert. +ManageRepositoriesDialog.Column.Primary=Primär +ManageRepositoriesDialog.Column.Enabled=Aktiviert +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Name +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Hinzufügen +ManageRepositoriesDialog.Button.Edit=Bearbeiten +ManageRepositoriesDialog.Button.Remove=Entfernen +ManageRepositoriesDialog.Button.SetPrimary=Als primär setzen +ManageRepositoriesDialog.Button.MoveUp=Hoch +ManageRepositoriesDialog.Button.MoveDown=Runter +ManageRepositoriesDialog.Button.ResetDefaults=Standards zurücksetzen +ManageRepositoriesDialog.Button.Save=Speichern +ManageRepositoriesDialog.Button.Cancel=Abbrechen +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repositories gespeichert +ManageRepositoriesDialog.Save.Done.Message=Marketplace-Repository-Einstellungen wurden in hop-config.json gespeichert. +ManageRepositoriesDialog.Error.Header=Repository-Fehler +ManageRepositoriesDialog.Error.Save=Marketplace-Konfiguration konnte nicht gespeichert werden +ManageRepositoriesDialog.Reset.Header=Repositories zurücksetzen +ManageRepositoriesDialog.Reset.Message=Repository-Liste durch Apache Repository (primär) und Maven Central ersetzen? +ManageRepositoriesDialog.Edit.Title.Add=Repository hinzufügen +ManageRepositoriesDialog.Edit.Title.Edit=Repository bearbeiten +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Name +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Benutzername +ManageRepositoriesDialog.Edit.Password=Passwort +ManageRepositoriesDialog.Edit.Enabled=Aktiviert (in Fallback-Kette verwenden) +ManageRepositoriesDialog.Edit.Primary=Primär (zuerst versucht) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id und URL sind erforderlich. +HopEnvironmentDialog.Shell.Title=Marketplace-Umgebung — {0} +HopEnvironmentDialog.Untitled=Unbenannt +HopEnvironmentDialog.Button.New=Neu +HopEnvironmentDialog.Button.Open=Öffnen… +HopEnvironmentDialog.Button.Save=Speichern +HopEnvironmentDialog.Button.SaveAs=Speichern unter… +HopEnvironmentDialog.Button.Validate=Validieren +HopEnvironmentDialog.Button.Validate.Tooltip=Lokale Installation mit dieser Umgebungsdatei vergleichen, ohne zu installieren +HopEnvironmentDialog.Button.Apply=Anwenden +HopEnvironmentDialog.Button.Apply.Tooltip=In dieser Umgebungsdatei deklarierte Plugins und Abhängigkeiten installieren +HopEnvironmentDialog.Button.Close=Schließen +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Abbrechen +HopEnvironmentDialog.Button.Add=Hinzufügen +HopEnvironmentDialog.Button.Edit=Bearbeiten +HopEnvironmentDialog.Button.Remove=Entfernen +HopEnvironmentDialog.Button.MoveUp=Hoch +HopEnvironmentDialog.Button.MoveDown=Runter +HopEnvironmentDialog.Button.AddFromCatalog=Aus Katalog… +HopEnvironmentDialog.Button.ImportRepos=Aus hop-config importieren… +HopEnvironmentDialog.Prune=Extras beim Anwenden entfernen +HopEnvironmentDialog.Prune.Tooltip=Beim Anwenden Marketplace-Plugins mit Receipt deinstallieren, die nicht in der Datei stehen +HopEnvironmentDialog.Strict=Strenge Validierung +HopEnvironmentDialog.Strict.Tooltip=Beim Validieren auch fehlschlagen, wenn zusätzliche Marketplace-Plugins jenseits der Datei installiert sind +HopEnvironmentDialog.Filter.Yaml=YAML-Umgebung (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON-Umgebung (*.json) +HopEnvironmentDialog.Tab.General=Allgemein +HopEnvironmentDialog.Tab.Repositories=Repositories +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Abhängigkeiten +HopEnvironmentDialog.General.Help=Legen Sie fest, welche Plugins und JARs diese Hop-Installation haben soll. Speichern Sie als hop-env.yaml für CI/Docker oder nutzen Sie Validieren / Anwenden in diesem Dialog. +HopEnvironmentDialog.General.Version=Spec-Version +HopEnvironmentDialog.General.HopVersion=Hop-Version +HopEnvironmentDialog.General.EnforceOnRun=Bei hop-run erzwingen +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Wenn true, kann hop-run fehlschlagen, wenn die Installation von dieser Datei abweicht (auch gesteuert durch -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=Die erste Zeile ist primär (zuerst versucht); spätere Zeilen sind Fallbacks. Diese Repositories liegen in der Umgebungsdatei (nicht hop-config). Passwörter möglichst nicht committen. +HopEnvironmentDialog.Plugins.Help=Zu installierende Marketplace-Plugins. Group ID leer lassen für org.apache.hop. Version kann beim Anwenden von der Hop-Version auf der Registerkarte Allgemein kommen. +HopEnvironmentDialog.Dependencies.Help=Optionale Maven-JARs (z. B. JDBC-Treiber), installiert unter dem Zielverzeichnis der Hop-Home (Standard lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Benutzername +HopEnvironmentDialog.Repo.Column.Password=Passwort +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Repository hinzufügen +HopEnvironmentDialog.Repo.Edit.Edit=Repository bearbeiten +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Version +HopEnvironmentDialog.Plugin.Edit.Add=Plugin hinzufügen +HopEnvironmentDialog.Plugin.Edit.Edit=Plugin bearbeiten +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Version +HopEnvironmentDialog.Dep.Column.Target=Ziel +HopEnvironmentDialog.Dep.Edit.Add=Abhängigkeit hinzufügen +HopEnvironmentDialog.Dep.Edit.Edit=Abhängigkeit bearbeiten +HopEnvironmentDialog.Dirty.Header=Ungespeicherte Änderungen +HopEnvironmentDialog.Dirty.Message=Änderungen an der Umgebungsdatei vor dem Fortfahren speichern? +HopEnvironmentDialog.SaveFirst.Header=Umgebungsdatei speichern? +HopEnvironmentDialog.SaveFirst.Message=Umgebungsdatei vor Validieren oder Anwenden speichern? +HopEnvironmentDialog.Validation.Header=Ungültige Umgebung +HopEnvironmentDialog.Validation.RepoIdUrl=Jedes Repository benötigt eine Id und URL. +HopEnvironmentDialog.Validation.PluginArtifact=Jedes Plugin benötigt eine Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Jede Abhängigkeit benötigt Group ID, Artifact ID und Version. +HopEnvironmentDialog.ImportRepos.Header=Repositories importieren +HopEnvironmentDialog.ImportRepos.Message=Repository-Liste durch aktivierte Repositories aus hop-config.json ersetzen? +HopEnvironmentDialog.Error.Header=Fehler im Umgebungeneditor +HopEnvironmentDialog.Error.Load=Umgebungsdatei konnte nicht geladen werden:\n{0} +HopEnvironmentDialog.Error.Save=Umgebungsdatei konnte nicht gespeichert werden:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Repositories konnten nicht aus hop-config importiert werden +AddPluginsFromCatalogDialog.Shell.Title=Plugins aus Katalog hinzufügen +AddPluginsFromCatalogDialog.Filter.Label=Filter +AddPluginsFromCatalogDialog.Button.Add=Auswahl hinzufügen +AddPluginsFromCatalogDialog.Button.Cancel=Abbrechen +AddPluginsFromCatalogDialog.Column.Name=Name +AddPluginsFromCatalogDialog.Column.Artifact=Artefakt +AddPluginsFromCatalogDialog.Column.Category=Kategorie +AddPluginsFromCatalogDialog.Column.Description=Beschreibung diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties new file mode 100644 index 00000000000..fbfe263eb56 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_en_US.properties @@ -0,0 +1,252 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… + +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace plugins +MarketplaceLifecycleEnv.Help=Plugin set is defined in a hop-env file. These options control when Hop checks that file for this lifecycle environment (dev can stay loose; production can enforce). +MarketplaceLifecycleEnv.EnvFile.Label=Environment file +MarketplaceLifecycleEnv.Button.Browse=Browse… +MarketplaceLifecycleEnv.Button.EditEnv=Edit… +MarketplaceLifecycleEnv.OnEnable.Label=On enable +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: no check; warn: log/dialog on drift; enforce: fail enabling the environment on drift +MarketplaceLifecycleEnv.Strict.Label=Strict (fail if extra marketplace plugins installed) +MarketplaceLifecycleEnv.Strict.Tooltip=When validating, also treat marketplace plugins with receipts that are not listed in the env file as drift +MarketplaceLifecycleEnv.AutoApply.Label=Auto-apply missing plugins on enable +MarketplaceLifecycleEnv.AutoApply.Tooltip=When enabled, run marketplace apply for missing plugins (use only on controlled machines; default off) + +MarketplaceDialog.Shell.Title=Plugin Marketplace +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Environment +MarketplaceDialog.Tab.Repositories=Repositories +MarketplaceDialog.Repository.Label=Primary repository +MarketplaceDialog.Repository.SetPrimary.Error=Could not set primary repository +MarketplaceDialog.Column.Name=Name +MarketplaceDialog.Column.Version=Version +MarketplaceDialog.Column.Artifact=Artifact +MarketplaceDialog.Column.Category=Category +MarketplaceDialog.Column.Repository=Repository +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Description +MarketplaceDialog.Repository.Apache=Apache (optional catalog) +MarketplaceDialog.Search.Label=Search +MarketplaceDialog.Search.Placeholder=Type 3+ characters to filter plugins… +MarketplaceDialog.Search.Clear=Clear search filter +MarketplaceDialog.Button.Install=Install +MarketplaceDialog.Button.Uninstall=Uninstall +MarketplaceDialog.Button.Refresh=Refresh +MarketplaceDialog.Button.Close=Close +MarketplaceDialog.ReposDirty.Header=Unsaved repository changes +MarketplaceDialog.ReposDirty.Close.Message=Repository settings were changed but not saved to hop-config.\n\nSave changes before closing? +MarketplaceDialog.ReposDirty.Refresh.Message=Repository settings were changed but not saved.\nPlugin discovery uses the saved configuration.\n\nSave changes before refreshing the plugin list? +MarketplaceDialog.ReposDirty.Reload.Error=Could not reload marketplace configuration from hop-config +MarketplaceDialog.Button.ManageRepos=Manage… +MarketplaceDialog.Button.BrowseEnv=Browse… +MarketplaceDialog.Button.EditEnv=Edit… +MarketplaceDialog.Button.EditEnv.Tooltip=Create or edit the environment file (repositories, plugins, dependencies) +MarketplaceDialog.Button.ValidateEnv=Validate +MarketplaceDialog.Button.ValidateEnv.Tooltip=Check the local install against the environment file without installing +MarketplaceDialog.Button.ApplyEnv=Apply +MarketplaceDialog.Button.ApplyEnv.Tooltip=Install plugins and dependencies declared in the environment file +MarketplaceDialog.EnvFile.Label=Environment file +MarketplaceDialog.EnvFile.Prune=Prune extras on apply +MarketplaceDialog.EnvFile.Prune.Tooltip=When applying, uninstall marketplace plugins that have a receipt but are not listed in the file +MarketplaceDialog.EnvFile.Strict=Strict validate +MarketplaceDialog.EnvFile.Strict.Tooltip=When validating, also fail if extra marketplace plugins are installed beyond the file +MarketplaceDialog.EnvFile.Filter.Env=Hop environment files (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=All files +MarketplaceDialog.EnvFile.Missing.Header=Environment file required +MarketplaceDialog.EnvFile.Missing.Message=Select a hop-env.yaml (or full-client-env.yaml) with Browse…, or type the path. +MarketplaceDialog.EnvFile.NotFound.Header=File not found +MarketplaceDialog.EnvFile.NotFound.Message=Environment file does not exist:\n{0} +MarketplaceDialog.Status.Installed=Installed +MarketplaceDialog.Status.Present=Present on disk +MarketplaceDialog.Status.NotInstalled=Not installed +MarketplaceDialog.Status.RestartHint=Restart Hop after install, apply, or uninstall so plugins are reloaded. Install tries the primary repository, then other enabled repositories. +MarketplaceDialog.Status.Installing=Installing {0}... +MarketplaceDialog.Status.Validating=Validating environment file... +MarketplaceDialog.Status.Applying=Applying environment file... +MarketplaceDialog.Status.Drift=Environment drift detected — use Apply to fix +MarketplaceDialog.Error.Header=Marketplace error +MarketplaceDialog.Error.ResolveHome=Could not determine the Hop installation directory. Start Hop from the install root using the hop launcher. +MarketplaceDialog.Install.Done.Header=Plugin installed +MarketplaceDialog.Install.Done.Message=Installed {0}.\n\nPlease restart Hop to load the plugin. +MarketplaceDialog.Install.Error=Failed to install {0} +MarketplaceDialog.Uninstall.Confirm.Header=Uninstall plugin +MarketplaceDialog.Uninstall.Confirm.Message=Remove marketplace-installed plugin {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Cannot uninstall +MarketplaceDialog.Uninstall.NoReceipt.Message=No marketplace install receipt for {0}. Only plugins installed via the marketplace can be uninstalled here. +MarketplaceDialog.Uninstall.Error=Failed to uninstall {0} +MarketplaceDialog.Validate.Ok.Header=Environment OK +MarketplaceDialog.Validate.Ok.Message=Local install matches:\n{0} +MarketplaceDialog.Validate.Drift.Header=Environment drift +MarketplaceDialog.Validate.Drift.Message=The local install does not match the environment file: +MarketplaceDialog.Validate.Drift.Hint=Use Apply to install missing plugins (or fix versions), then restart Hop. +MarketplaceDialog.Validate.Error=Failed to validate the environment file +MarketplaceDialog.Apply.PruneConfirm.Header=Prune marketplace plugins? +MarketplaceDialog.Apply.PruneConfirm.Message=Apply with prune will uninstall marketplace plugins that are not listed in the environment file.\n\nContinue? +MarketplaceDialog.Apply.Disabled=Marketplace is disabled in hop-config.json. +MarketplaceDialog.Apply.Done.Header=Environment applied +MarketplaceDialog.Apply.Done.Message=Applied:\n{0}\n\nPlease restart Hop so new plugins are loaded. +MarketplaceDialog.Apply.Error=Failed to apply the environment file + +ManageRepositoriesDialog.Shell.Title=Marketplace repositories +ManageRepositoriesDialog.Help=Primary repo is tried first for installs; other enabled repos are fallbacks. Enable Browse to list zips live on the Plugins tab.\nImport/Export shareable repository YAML. Edit connection fields and plugin metadata (name, category, version pin, Hop range) via Add/Edit repository.\nDefaults: Apache Repository then Maven Central. Settings: hop-config.json under '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Primary +ManageRepositoriesDialog.Column.Enabled=Enabled +ManageRepositoriesDialog.Column.Browse=Browse +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Name +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Add +ManageRepositoriesDialog.Button.Import=Import… +ManageRepositoriesDialog.Button.Export=Export… +ManageRepositoriesDialog.Button.Edit=Edit +ManageRepositoriesDialog.Button.Remove=Remove +ManageRepositoriesDialog.Button.SetPrimary=Set primary +ManageRepositoriesDialog.Button.MoveUp=Up +ManageRepositoriesDialog.Button.MoveDown=Down +ManageRepositoriesDialog.Button.ResetDefaults=Reset defaults +ManageRepositoriesDialog.Button.Save=Save +ManageRepositoriesDialog.Button.Cancel=Cancel +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repositories saved +ManageRepositoriesDialog.Save.Done.Message=Marketplace repository settings were saved to hop-config.json. +ManageRepositoriesDialog.Error.Header=Repository error +ManageRepositoriesDialog.Error.Save=Could not save marketplace configuration +ManageRepositoriesDialog.Reset.Header=Reset repositories +ManageRepositoriesDialog.Reset.Message=Replace the repository list with Apache Repository (primary) and Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Add repository +ManageRepositoriesDialog.Edit.Title.Edit=Edit repository +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Name +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Username +ManageRepositoriesDialog.Edit.Password=Password +ManageRepositoriesDialog.Edit.Enabled=Enabled (use in fallback chain) +ManageRepositoriesDialog.Edit.Primary=Primary (tried first) +ManageRepositoriesDialog.Edit.Browse=Browse (list zips in query / Plugins tab) +ManageRepositoriesDialog.Edit.Browse.Tooltip=When enabled, marketplace query and the Plugins tab always include a live list of *.zip artifacts from this repository (Nexus search). Optional catalog URL overrides live search. +ManageRepositoriesDialog.Edit.CatalogUrl=Catalog URL +ManageRepositoriesDialog.Edit.SearchQuery=Search query +ManageRepositoriesDialog.Edit.GroupIdFilter=Group ID filter +ManageRepositoriesDialog.Edit.IncludeSnapshots=Include SNAPSHOT versions in discovery +ManageRepositoriesDialog.Edit.IdUrlRequired=Id and URL are required. +ManageRepositoriesDialog.Filter.RepoDef=Repository definitions (*.yaml, *.yml, *.json) +ManageRepositoriesDialog.Filter.Yaml=YAML (*.yaml, *.yml) +ManageRepositoriesDialog.Filter.Json=JSON (*.json) +ManageRepositoriesDialog.Import.Done.Header=Repository imported +ManageRepositoriesDialog.Import.Done.Message=Imported/updated repository ''{0}''. Click Save to persist hop-config. +ManageRepositoriesDialog.Import.Error=Could not import repository definition +ManageRepositoriesDialog.Export.Select=Select a repository row to export. +ManageRepositoriesDialog.Export.Done.Header=Repository exported +ManageRepositoriesDialog.Export.Done.Message=Wrote:\n{0} +ManageRepositoriesDialog.Export.Error=Could not export repository definition +ManageRepositoriesDialog.Edit.Tab.General=General +ManageRepositoriesDialog.Tab.PluginMetadata=Plugin metadata +ManageRepositoriesDialog.Plugins.Hint=Optional metadata for plugins from this repository (enriches live browse: name, category, version pin, min/max Hop version). Applied when you click OK. +ManageRepositoriesDialog.Plugins.Column.Group=Group ID +ManageRepositoriesDialog.Plugins.Column.Artifact=Artifact ID +ManageRepositoriesDialog.Plugins.Column.Version=Version pin +ManageRepositoriesDialog.Plugins.Column.Name=Name +ManageRepositoriesDialog.Plugins.Column.Category=Category +ManageRepositoriesDialog.Plugins.Column.Description=Description +ManageRepositoriesDialog.Plugins.Column.MinHop=Min Hop version +ManageRepositoriesDialog.Plugins.Column.MaxHop=Max Hop version +ManageRepositoriesDialog.Button.AddPlugin=Add plugin +ManageRepositoriesDialog.Button.RemovePlugin=Remove plugin +HopEnvironmentDialog.Shell.Title=Marketplace environment — {0} +HopEnvironmentDialog.Untitled=Untitled +HopEnvironmentDialog.Button.New=New +HopEnvironmentDialog.Button.Open=Open… +HopEnvironmentDialog.Button.Save=Save +HopEnvironmentDialog.Button.SaveAs=Save As… +HopEnvironmentDialog.Button.Validate=Validate +HopEnvironmentDialog.Button.Validate.Tooltip=Check the local install against this environment file without installing +HopEnvironmentDialog.Button.Apply=Apply +HopEnvironmentDialog.Button.Apply.Tooltip=Install plugins and dependencies declared in this environment file +HopEnvironmentDialog.Button.Close=Close +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Cancel +HopEnvironmentDialog.Button.Add=Add +HopEnvironmentDialog.Button.Edit=Edit +HopEnvironmentDialog.Button.Remove=Remove +HopEnvironmentDialog.Button.MoveUp=Up +HopEnvironmentDialog.Button.MoveDown=Down +HopEnvironmentDialog.Button.AddFromCatalog=From catalog… +HopEnvironmentDialog.Button.ImportRepos=Import from hop-config… +HopEnvironmentDialog.Prune=Prune extras on apply +HopEnvironmentDialog.Prune.Tooltip=When applying, uninstall marketplace plugins that have a receipt but are not listed in the file +HopEnvironmentDialog.Strict=Strict validate +HopEnvironmentDialog.Strict.Tooltip=When validating, also fail if extra marketplace plugins are installed beyond the file +HopEnvironmentDialog.Filter.Yaml=YAML environment (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON environment (*.json) +HopEnvironmentDialog.Tab.General=General +HopEnvironmentDialog.Tab.Repositories=Repositories +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dependencies +HopEnvironmentDialog.General.Help=Declare which plugins and jars this Hop install should have. Save as hop-env.yaml for CI/Docker, or use Validate / Apply from this dialog. +HopEnvironmentDialog.General.Version=Spec version +HopEnvironmentDialog.General.HopVersion=Hop version +HopEnvironmentDialog.General.EnforceOnRun=Enforce on hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=When true, hop-run can fail if the install drifts from this file (also controlled by -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=First row is primary (tried first); later rows are fallbacks. These repositories live in the environment file (not hop-config). Prefer not to commit passwords. +HopEnvironmentDialog.Plugins.Help=Marketplace plugins to install. Leave Group ID blank to use org.apache.hop. Version can default from Hop version on the General tab when applying. +HopEnvironmentDialog.Dependencies.Help=Optional Maven jars (for example JDBC drivers) installed under the Hop home target directory (default lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Username +HopEnvironmentDialog.Repo.Column.Password=Password +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Add repository +HopEnvironmentDialog.Repo.Edit.Edit=Edit repository +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Version +HopEnvironmentDialog.Plugin.Edit.Add=Add plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Edit plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Version +HopEnvironmentDialog.Dep.Column.Target=Target +HopEnvironmentDialog.Dep.Edit.Add=Add dependency +HopEnvironmentDialog.Dep.Edit.Edit=Edit dependency +HopEnvironmentDialog.Dirty.Header=Unsaved changes +HopEnvironmentDialog.Dirty.Message=Save changes to the environment file before continuing? +HopEnvironmentDialog.SaveFirst.Header=Save environment file? +HopEnvironmentDialog.SaveFirst.Message=Save the environment file before Validate or Apply? +HopEnvironmentDialog.Validation.Header=Invalid environment +HopEnvironmentDialog.Validation.RepoIdUrl=Each repository needs an Id and URL. +HopEnvironmentDialog.Validation.PluginArtifact=Each plugin needs an Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Each dependency needs Group ID, Artifact ID, and Version. +HopEnvironmentDialog.ImportRepos.Header=Import repositories +HopEnvironmentDialog.ImportRepos.Message=Replace the repository list with enabled repositories from hop-config.json? +HopEnvironmentDialog.Error.Header=Environment editor error +HopEnvironmentDialog.Error.Load=Could not load environment file:\n{0} +HopEnvironmentDialog.Error.Save=Could not save environment file:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Could not import repositories from hop-config + +AddPluginsFromCatalogDialog.Shell.Title=Add plugins from catalog +AddPluginsFromCatalogDialog.Filter.Label=Filter +AddPluginsFromCatalogDialog.Button.Add=Add selected +AddPluginsFromCatalogDialog.Button.Cancel=Cancel +AddPluginsFromCatalogDialog.Column.Name=Name +AddPluginsFromCatalogDialog.Column.Artifact=Artifact +AddPluginsFromCatalogDialog.Column.Category=Category +AddPluginsFromCatalogDialog.Column.Description=Description diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_AR.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_AR.properties new file mode 100644 index 00000000000..2035e764fd5 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_AR.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugins de Marketplace +MarketplaceLifecycleEnv.Help=El conjunto de plugins se define en un archivo hop-env. Estas opciones controlan cuándo Hop verifica ese archivo para este entorno de ciclo de vida (desarrollo flexible; producción estricta). +MarketplaceLifecycleEnv.EnvFile.Label=Archivo de entorno +MarketplaceLifecycleEnv.Button.Browse=Examinar… +MarketplaceLifecycleEnv.Button.EditEnv=Editar… +MarketplaceLifecycleEnv.OnEnable.Label=Al habilitar +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: sin comprobación; warn: registro/diálogo si hay divergencia; enforce: fallar al habilitar el entorno si hay divergencia +MarketplaceLifecycleEnv.Strict.Label=Estricto (fallar si hay plugins de marketplace extra instalados) +MarketplaceLifecycleEnv.Strict.Tooltip=Al validar, tratar también como divergencia los plugins de marketplace con recibo que no estén en el archivo de entorno +MarketplaceLifecycleEnv.AutoApply.Label=Aplicar automáticamente plugins faltantes al habilitar +MarketplaceLifecycleEnv.AutoApply.Tooltip=Si está habilitado, ejecuta marketplace apply para plugins faltantes (solo en máquinas controladas; desactivado por defecto) +MarketplaceDialog.Shell.Title=Marketplace de plugins +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Entorno +MarketplaceDialog.Tab.Repositories=Repositorios +MarketplaceDialog.Repository.Label=Repositorio principal +MarketplaceDialog.Repository.SetPrimary.Error=No se pudo establecer el repositorio principal +MarketplaceDialog.Column.Name=Nombre +MarketplaceDialog.Column.Artifact=Artefacto +MarketplaceDialog.Column.Category=Categoría +MarketplaceDialog.Column.Status=Estado +MarketplaceDialog.Column.Description=Descripción +MarketplaceDialog.Search.Label=Buscar +MarketplaceDialog.Search.Placeholder=Escriba 3+ caracteres para filtrar plugins… +MarketplaceDialog.Search.Clear=Borrar filtro de búsqueda +MarketplaceDialog.Button.Install=Instalar +MarketplaceDialog.Button.Uninstall=Desinstalar +MarketplaceDialog.Button.Refresh=Actualizar +MarketplaceDialog.Button.Close=Cerrar +MarketplaceDialog.Button.ManageRepos=Administrar… +MarketplaceDialog.Button.BrowseEnv=Examinar… +MarketplaceDialog.Button.EditEnv=Editar… +MarketplaceDialog.Button.EditEnv.Tooltip=Crear o editar el archivo de entorno (repositorios, plugins, dependencias) +MarketplaceDialog.Button.ValidateEnv=Validar +MarketplaceDialog.Button.ValidateEnv.Tooltip=Comprobar la instalación local con el archivo de entorno sin instalar +MarketplaceDialog.Button.ApplyEnv=Aplicar +MarketplaceDialog.Button.ApplyEnv.Tooltip=Instalar plugins y dependencias declarados en el archivo de entorno +MarketplaceDialog.EnvFile.Label=Archivo de entorno +MarketplaceDialog.EnvFile.Prune=Eliminar extras al aplicar +MarketplaceDialog.EnvFile.Prune.Tooltip=Al aplicar, desinstalar plugins de marketplace con recibo que no estén en el archivo +MarketplaceDialog.EnvFile.Strict=Validación estricta +MarketplaceDialog.EnvFile.Strict.Tooltip=Al validar, fallar también si hay plugins de marketplace instalados más allá del archivo +MarketplaceDialog.EnvFile.Filter.Env=Archivos de entorno Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Todos los archivos +MarketplaceDialog.EnvFile.Missing.Header=Archivo de entorno requerido +MarketplaceDialog.EnvFile.Missing.Message=Seleccione un hop-env.yaml (o full-client-env.yaml) con Examinar…, o escriba la ruta. +MarketplaceDialog.EnvFile.NotFound.Header=Archivo no encontrado +MarketplaceDialog.EnvFile.NotFound.Message=El archivo de entorno no existe:\n{0} +MarketplaceDialog.Status.Installed=Instalado +MarketplaceDialog.Status.Present=Presente en disco +MarketplaceDialog.Status.NotInstalled=No instalado +MarketplaceDialog.Status.RestartHint=Reinicie Hop tras instalar, aplicar o desinstalar para recargar los plugins. La instalación prueba el repositorio principal y luego otros repositorios habilitados. +MarketplaceDialog.Status.Installing=Instalando {0}... +MarketplaceDialog.Status.Validating=Validando archivo de entorno... +MarketplaceDialog.Status.Applying=Aplicando archivo de entorno... +MarketplaceDialog.Status.Drift=Divergencia de entorno detectada — use Aplicar para corregir +MarketplaceDialog.Error.Header=Error de Marketplace +MarketplaceDialog.Error.ResolveHome=No se pudo determinar el directorio de instalación de Hop. Inicie Hop desde la raíz de instalación con el lanzador de Hop. +MarketplaceDialog.Install.Done.Header=Plugin instalado +MarketplaceDialog.Install.Done.Message=Instalado {0}.\n\nReinicie Hop para cargar el plugin. +MarketplaceDialog.Install.Error=Error al instalar {0} +MarketplaceDialog.Uninstall.Confirm.Header=Desinstalar plugin +MarketplaceDialog.Uninstall.Confirm.Message=¿Quitar el plugin instalado por marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=No se puede desinstalar +MarketplaceDialog.Uninstall.NoReceipt.Message=No hay recibo de instalación de marketplace para {0}. Solo se pueden desinstalar aquí plugins instalados vía marketplace. +MarketplaceDialog.Uninstall.Error=Error al desinstalar {0} +MarketplaceDialog.Validate.Ok.Header=Entorno correcto +MarketplaceDialog.Validate.Ok.Message=La instalación local coincide:\n{0} +MarketplaceDialog.Validate.Drift.Header=Divergencia de entorno +MarketplaceDialog.Validate.Drift.Message=La instalación local no coincide con el archivo de entorno: +MarketplaceDialog.Validate.Drift.Hint=Use Aplicar para instalar plugins faltantes (o corregir versiones) y reinicie Hop. +MarketplaceDialog.Validate.Error=Error al validar el archivo de entorno +MarketplaceDialog.Apply.PruneConfirm.Header=¿Podar plugins de marketplace? +MarketplaceDialog.Apply.PruneConfirm.Message=Aplicar con poda desinstalará plugins de marketplace que no estén en el archivo de entorno.\n\n¿Continuar? +MarketplaceDialog.Apply.Disabled=Marketplace está deshabilitado en hop-config.json. +MarketplaceDialog.Apply.Done.Header=Entorno aplicado +MarketplaceDialog.Apply.Done.Message=Aplicado:\n{0}\n\nReinicie Hop para cargar los nuevos plugins. +MarketplaceDialog.Apply.Error=Error al aplicar el archivo de entorno +ManageRepositoriesDialog.Shell.Title=Repositorios de Marketplace +ManageRepositoriesDialog.Help=El repositorio principal se prueba primero. Los demás repositorios habilitados son de respaldo.\nPredeterminados: Apache Repository (repository.apache.org) y luego Maven Central.\nLa configuración se guarda en hop-config.json bajo '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principal +ManageRepositoriesDialog.Column.Enabled=Habilitado +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nombre +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Agregar +ManageRepositoriesDialog.Button.Edit=Editar +ManageRepositoriesDialog.Button.Remove=Quitar +ManageRepositoriesDialog.Button.SetPrimary=Establecer principal +ManageRepositoriesDialog.Button.MoveUp=Arriba +ManageRepositoriesDialog.Button.MoveDown=Abajo +ManageRepositoriesDialog.Button.ResetDefaults=Restablecer valores predeterminados +ManageRepositoriesDialog.Button.Save=Guardar +ManageRepositoriesDialog.Button.Cancel=Cancelar +ManageRepositoriesDialog.Button.Ok=Aceptar +ManageRepositoriesDialog.Save.Done.Header=Repositorios guardados +ManageRepositoriesDialog.Save.Done.Message=La configuración de repositorios de Marketplace se guardó en hop-config.json. +ManageRepositoriesDialog.Error.Header=Error de repositorio +ManageRepositoriesDialog.Error.Save=No se pudo guardar la configuración de Marketplace +ManageRepositoriesDialog.Reset.Header=Restablecer repositorios +ManageRepositoriesDialog.Reset.Message=¿Reemplazar la lista de repositorios por Apache Repository (principal) y Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Agregar repositorio +ManageRepositoriesDialog.Edit.Title.Edit=Editar repositorio +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nombre +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Usuario +ManageRepositoriesDialog.Edit.Password=Contraseña +ManageRepositoriesDialog.Edit.Enabled=Habilitado (usar en cadena de respaldo) +ManageRepositoriesDialog.Edit.Primary=Principal (se prueba primero) +ManageRepositoriesDialog.Edit.IdUrlRequired=Se requieren Id y URL. +HopEnvironmentDialog.Shell.Title=Entorno Marketplace — {0} +HopEnvironmentDialog.Untitled=Sin título +HopEnvironmentDialog.Button.New=Nuevo +HopEnvironmentDialog.Button.Open=Abrir… +HopEnvironmentDialog.Button.Save=Guardar +HopEnvironmentDialog.Button.SaveAs=Guardar como… +HopEnvironmentDialog.Button.Validate=Validar +HopEnvironmentDialog.Button.Validate.Tooltip=Comprobar la instalación local con este archivo de entorno sin instalar +HopEnvironmentDialog.Button.Apply=Aplicar +HopEnvironmentDialog.Button.Apply.Tooltip=Instalar plugins y dependencias declarados en este archivo de entorno +HopEnvironmentDialog.Button.Close=Cerrar +HopEnvironmentDialog.Button.Ok=Aceptar +HopEnvironmentDialog.Button.Cancel=Cancelar +HopEnvironmentDialog.Button.Add=Agregar +HopEnvironmentDialog.Button.Edit=Editar +HopEnvironmentDialog.Button.Remove=Quitar +HopEnvironmentDialog.Button.MoveUp=Arriba +HopEnvironmentDialog.Button.MoveDown=Abajo +HopEnvironmentDialog.Button.AddFromCatalog=Desde catálogo… +HopEnvironmentDialog.Button.ImportRepos=Importar desde hop-config… +HopEnvironmentDialog.Prune=Eliminar extras al aplicar +HopEnvironmentDialog.Prune.Tooltip=Al aplicar, desinstalar plugins de marketplace con recibo que no estén en el archivo +HopEnvironmentDialog.Strict=Validación estricta +HopEnvironmentDialog.Strict.Tooltip=Al validar, fallar también si hay plugins de marketplace instalados más allá del archivo +HopEnvironmentDialog.Filter.Yaml=Entorno YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Entorno JSON (*.json) +HopEnvironmentDialog.Tab.General=General +HopEnvironmentDialog.Tab.Repositories=Repositorios +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dependencias +HopEnvironmentDialog.General.Help=Declare qué plugins y jars debe tener esta instalación de Hop. Guarde como hop-env.yaml para CI/Docker, o use Validar / Aplicar en este diálogo. +HopEnvironmentDialog.General.Version=Versión de especificación +HopEnvironmentDialog.General.HopVersion=Versión de Hop +HopEnvironmentDialog.General.EnforceOnRun=Forzar en hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Si es true, hop-run puede fallar si la instalación se desvía de este archivo (también controlado por -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=La primera fila es principal (se prueba primero); las siguientes son de respaldo. Estos repositorios viven en el archivo de entorno (no hop-config). Prefiera no confirmar contraseñas. +HopEnvironmentDialog.Plugins.Help=Plugins de marketplace a instalar. Deje Group ID en blanco para usar org.apache.hop. La versión puede tomarse de la versión de Hop en la pestaña General al aplicar. +HopEnvironmentDialog.Dependencies.Help=Jars Maven opcionales (por ejemplo controladores JDBC) instalados bajo el directorio destino de Hop home (predeterminado lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Usuario +HopEnvironmentDialog.Repo.Column.Password=Contraseña +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Agregar repositorio +HopEnvironmentDialog.Repo.Edit.Edit=Editar repositorio +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versión +HopEnvironmentDialog.Plugin.Edit.Add=Agregar plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Editar plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versión +HopEnvironmentDialog.Dep.Column.Target=Destino +HopEnvironmentDialog.Dep.Edit.Add=Agregar dependencia +HopEnvironmentDialog.Dep.Edit.Edit=Editar dependencia +HopEnvironmentDialog.Dirty.Header=Cambios sin guardar +HopEnvironmentDialog.Dirty.Message=¿Guardar los cambios del archivo de entorno antes de continuar? +HopEnvironmentDialog.SaveFirst.Header=¿Guardar archivo de entorno? +HopEnvironmentDialog.SaveFirst.Message=¿Guardar el archivo de entorno antes de Validar o Aplicar? +HopEnvironmentDialog.Validation.Header=Entorno no válido +HopEnvironmentDialog.Validation.RepoIdUrl=Cada repositorio necesita un Id y una URL. +HopEnvironmentDialog.Validation.PluginArtifact=Cada plugin necesita un Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Cada dependencia necesita Group ID, Artifact ID y Version. +HopEnvironmentDialog.ImportRepos.Header=Importar repositorios +HopEnvironmentDialog.ImportRepos.Message=¿Reemplazar la lista de repositorios con los repositorios habilitados de hop-config.json? +HopEnvironmentDialog.Error.Header=Error del editor de entorno +HopEnvironmentDialog.Error.Load=No se pudo cargar el archivo de entorno:\n{0} +HopEnvironmentDialog.Error.Save=No se pudo guardar el archivo de entorno:\n{0} +HopEnvironmentDialog.Error.ImportRepos=No se pudieron importar repositorios desde hop-config +AddPluginsFromCatalogDialog.Shell.Title=Agregar plugins del catálogo +AddPluginsFromCatalogDialog.Filter.Label=Filtro +AddPluginsFromCatalogDialog.Button.Add=Agregar seleccionados +AddPluginsFromCatalogDialog.Button.Cancel=Cancelar +AddPluginsFromCatalogDialog.Column.Name=Nombre +AddPluginsFromCatalogDialog.Column.Artifact=Artefacto +AddPluginsFromCatalogDialog.Column.Category=Categoría +AddPluginsFromCatalogDialog.Column.Description=Descripción diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_ES.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_ES.properties new file mode 100644 index 00000000000..3ecbb0de7df --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_es_ES.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugins de Marketplace +MarketplaceLifecycleEnv.Help=El conjunto de plugins se define en un archivo hop-env. Estas opciones controlan cuándo Hop comprueba ese archivo para este entorno de ciclo de vida (desarrollo flexible; producción estricta). +MarketplaceLifecycleEnv.EnvFile.Label=Archivo de entorno +MarketplaceLifecycleEnv.Button.Browse=Examinar… +MarketplaceLifecycleEnv.Button.EditEnv=Editar… +MarketplaceLifecycleEnv.OnEnable.Label=Al habilitar +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: sin comprobación; warn: registro/diálogo si hay divergencia; enforce: fallar al habilitar el entorno si hay divergencia +MarketplaceLifecycleEnv.Strict.Label=Estricto (fallar si hay plugins de marketplace extra instalados) +MarketplaceLifecycleEnv.Strict.Tooltip=Al validar, tratar también como divergencia los plugins de marketplace con recibo que no estén en el archivo de entorno +MarketplaceLifecycleEnv.AutoApply.Label=Aplicar automáticamente plugins faltantes al habilitar +MarketplaceLifecycleEnv.AutoApply.Tooltip=Si está habilitado, ejecuta marketplace apply para plugins faltantes (solo en máquinas controladas; desactivado por defecto) +MarketplaceDialog.Shell.Title=Marketplace de plugins +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Entorno +MarketplaceDialog.Tab.Repositories=Repositorios +MarketplaceDialog.Repository.Label=Repositorio principal +MarketplaceDialog.Repository.SetPrimary.Error=No se pudo establecer el repositorio principal +MarketplaceDialog.Column.Name=Nombre +MarketplaceDialog.Column.Artifact=Artefacto +MarketplaceDialog.Column.Category=Categoría +MarketplaceDialog.Column.Status=Estado +MarketplaceDialog.Column.Description=Descripción +MarketplaceDialog.Search.Label=Buscar +MarketplaceDialog.Search.Placeholder=Escriba 3+ caracteres para filtrar plugins… +MarketplaceDialog.Search.Clear=Borrar filtro de búsqueda +MarketplaceDialog.Button.Install=Instalar +MarketplaceDialog.Button.Uninstall=Desinstalar +MarketplaceDialog.Button.Refresh=Actualizar +MarketplaceDialog.Button.Close=Cerrar +MarketplaceDialog.Button.ManageRepos=Administrar… +MarketplaceDialog.Button.BrowseEnv=Examinar… +MarketplaceDialog.Button.EditEnv=Editar… +MarketplaceDialog.Button.EditEnv.Tooltip=Crear o editar el archivo de entorno (repositorios, plugins, dependencias) +MarketplaceDialog.Button.ValidateEnv=Validar +MarketplaceDialog.Button.ValidateEnv.Tooltip=Comprobar la instalación local con el archivo de entorno sin instalar +MarketplaceDialog.Button.ApplyEnv=Aplicar +MarketplaceDialog.Button.ApplyEnv.Tooltip=Instalar plugins y dependencias declarados en el archivo de entorno +MarketplaceDialog.EnvFile.Label=Archivo de entorno +MarketplaceDialog.EnvFile.Prune=Eliminar extras al aplicar +MarketplaceDialog.EnvFile.Prune.Tooltip=Al aplicar, desinstalar plugins de marketplace con recibo que no estén en el archivo +MarketplaceDialog.EnvFile.Strict=Validación estricta +MarketplaceDialog.EnvFile.Strict.Tooltip=Al validar, fallar también si hay plugins de marketplace instalados más allá del archivo +MarketplaceDialog.EnvFile.Filter.Env=Archivos de entorno Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Todos los archivos +MarketplaceDialog.EnvFile.Missing.Header=Archivo de entorno requerido +MarketplaceDialog.EnvFile.Missing.Message=Seleccione un hop-env.yaml (o full-client-env.yaml) con Examinar…, o escriba la ruta. +MarketplaceDialog.EnvFile.NotFound.Header=Archivo no encontrado +MarketplaceDialog.EnvFile.NotFound.Message=El archivo de entorno no existe:\n{0} +MarketplaceDialog.Status.Installed=Instalado +MarketplaceDialog.Status.Present=Presente en disco +MarketplaceDialog.Status.NotInstalled=No instalado +MarketplaceDialog.Status.RestartHint=Reinicie Hop tras instalar, aplicar o desinstalar para recargar los plugins. La instalación prueba el repositorio principal y luego otros repositorios habilitados. +MarketplaceDialog.Status.Installing=Instalando {0}... +MarketplaceDialog.Status.Validating=Validando archivo de entorno... +MarketplaceDialog.Status.Applying=Aplicando archivo de entorno... +MarketplaceDialog.Status.Drift=Divergencia de entorno detectada — use Aplicar para corregir +MarketplaceDialog.Error.Header=Error de Marketplace +MarketplaceDialog.Error.ResolveHome=No se pudo determinar el directorio de instalación de Hop. Inicie Hop desde la raíz de instalación con el lanzador de Hop. +MarketplaceDialog.Install.Done.Header=Plugin instalado +MarketplaceDialog.Install.Done.Message=Instalado {0}.\n\nReinicie Hop para cargar el plugin. +MarketplaceDialog.Install.Error=Error al instalar {0} +MarketplaceDialog.Uninstall.Confirm.Header=Desinstalar plugin +MarketplaceDialog.Uninstall.Confirm.Message=¿Quitar el plugin instalado por marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=No se puede desinstalar +MarketplaceDialog.Uninstall.NoReceipt.Message=No hay recibo de instalación de marketplace para {0}. Solo se pueden desinstalar aquí plugins instalados vía marketplace. +MarketplaceDialog.Uninstall.Error=Error al desinstalar {0} +MarketplaceDialog.Validate.Ok.Header=Entorno correcto +MarketplaceDialog.Validate.Ok.Message=La instalación local coincide:\n{0} +MarketplaceDialog.Validate.Drift.Header=Divergencia de entorno +MarketplaceDialog.Validate.Drift.Message=La instalación local no coincide con el archivo de entorno: +MarketplaceDialog.Validate.Drift.Hint=Use Aplicar para instalar plugins faltantes (o corregir versiones) y reinicie Hop. +MarketplaceDialog.Validate.Error=Error al validar el archivo de entorno +MarketplaceDialog.Apply.PruneConfirm.Header=¿Podar plugins de marketplace? +MarketplaceDialog.Apply.PruneConfirm.Message=Aplicar con poda desinstalará plugins de marketplace que no estén en el archivo de entorno.\n\n¿Continuar? +MarketplaceDialog.Apply.Disabled=Marketplace está deshabilitado en hop-config.json. +MarketplaceDialog.Apply.Done.Header=Entorno aplicado +MarketplaceDialog.Apply.Done.Message=Aplicado:\n{0}\n\nReinicie Hop para cargar los nuevos plugins. +MarketplaceDialog.Apply.Error=Error al aplicar el archivo de entorno +ManageRepositoriesDialog.Shell.Title=Repositorios de Marketplace +ManageRepositoriesDialog.Help=El repositorio principal se prueba primero. Los demás repositorios habilitados son de respaldo.\nPredeterminados: Apache Repository (repository.apache.org) y luego Maven Central.\nLa configuración se guarda en hop-config.json bajo '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principal +ManageRepositoriesDialog.Column.Enabled=Habilitado +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nombre +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Añadir +ManageRepositoriesDialog.Button.Edit=Editar +ManageRepositoriesDialog.Button.Remove=Quitar +ManageRepositoriesDialog.Button.SetPrimary=Establecer principal +ManageRepositoriesDialog.Button.MoveUp=Arriba +ManageRepositoriesDialog.Button.MoveDown=Abajo +ManageRepositoriesDialog.Button.ResetDefaults=Restablecer valores predeterminados +ManageRepositoriesDialog.Button.Save=Guardar +ManageRepositoriesDialog.Button.Cancel=Cancelar +ManageRepositoriesDialog.Button.Ok=Aceptar +ManageRepositoriesDialog.Save.Done.Header=Repositorios guardados +ManageRepositoriesDialog.Save.Done.Message=La configuración de repositorios de Marketplace se guardó en hop-config.json. +ManageRepositoriesDialog.Error.Header=Error de repositorio +ManageRepositoriesDialog.Error.Save=No se pudo guardar la configuración de Marketplace +ManageRepositoriesDialog.Reset.Header=Restablecer repositorios +ManageRepositoriesDialog.Reset.Message=¿Reemplazar la lista de repositorios por Apache Repository (principal) y Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Añadir repositorio +ManageRepositoriesDialog.Edit.Title.Edit=Editar repositorio +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nombre +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Usuario +ManageRepositoriesDialog.Edit.Password=Contraseña +ManageRepositoriesDialog.Edit.Enabled=Habilitado (usar en cadena de respaldo) +ManageRepositoriesDialog.Edit.Primary=Principal (se prueba primero) +ManageRepositoriesDialog.Edit.IdUrlRequired=Se requieren Id y URL. +HopEnvironmentDialog.Shell.Title=Entorno Marketplace — {0} +HopEnvironmentDialog.Untitled=Sin título +HopEnvironmentDialog.Button.New=Nuevo +HopEnvironmentDialog.Button.Open=Abrir… +HopEnvironmentDialog.Button.Save=Guardar +HopEnvironmentDialog.Button.SaveAs=Guardar como… +HopEnvironmentDialog.Button.Validate=Validar +HopEnvironmentDialog.Button.Validate.Tooltip=Comprobar la instalación local con este archivo de entorno sin instalar +HopEnvironmentDialog.Button.Apply=Aplicar +HopEnvironmentDialog.Button.Apply.Tooltip=Instalar plugins y dependencias declarados en este archivo de entorno +HopEnvironmentDialog.Button.Close=Cerrar +HopEnvironmentDialog.Button.Ok=Aceptar +HopEnvironmentDialog.Button.Cancel=Cancelar +HopEnvironmentDialog.Button.Add=Agregar +HopEnvironmentDialog.Button.Edit=Editar +HopEnvironmentDialog.Button.Remove=Quitar +HopEnvironmentDialog.Button.MoveUp=Arriba +HopEnvironmentDialog.Button.MoveDown=Abajo +HopEnvironmentDialog.Button.AddFromCatalog=Desde catálogo… +HopEnvironmentDialog.Button.ImportRepos=Importar desde hop-config… +HopEnvironmentDialog.Prune=Eliminar extras al aplicar +HopEnvironmentDialog.Prune.Tooltip=Al aplicar, desinstalar plugins de marketplace con recibo que no estén en el archivo +HopEnvironmentDialog.Strict=Validación estricta +HopEnvironmentDialog.Strict.Tooltip=Al validar, fallar también si hay plugins de marketplace instalados más allá del archivo +HopEnvironmentDialog.Filter.Yaml=Entorno YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Entorno JSON (*.json) +HopEnvironmentDialog.Tab.General=General +HopEnvironmentDialog.Tab.Repositories=Repositorios +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dependencias +HopEnvironmentDialog.General.Help=Declare qué plugins y jars debe tener esta instalación de Hop. Guarde como hop-env.yaml para CI/Docker, o use Validar / Aplicar en este diálogo. +HopEnvironmentDialog.General.Version=Versión de especificación +HopEnvironmentDialog.General.HopVersion=Versión de Hop +HopEnvironmentDialog.General.EnforceOnRun=Forzar en hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Si es true, hop-run puede fallar si la instalación se desvía de este archivo (también controlado por -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=La primera fila es principal (se prueba primero); las siguientes son de respaldo. Estos repositorios viven en el archivo de entorno (no hop-config). Prefiera no confirmar contraseñas. +HopEnvironmentDialog.Plugins.Help=Plugins de marketplace a instalar. Deje Group ID en blanco para usar org.apache.hop. La versión puede tomarse de la versión de Hop en la pestaña General al aplicar. +HopEnvironmentDialog.Dependencies.Help=Jars Maven opcionales (por ejemplo controladores JDBC) instalados bajo el directorio destino de Hop home (predeterminado lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Usuario +HopEnvironmentDialog.Repo.Column.Password=Contraseña +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Añadir repositorio +HopEnvironmentDialog.Repo.Edit.Edit=Editar repositorio +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versión +HopEnvironmentDialog.Plugin.Edit.Add=Añadir plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Editar plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versión +HopEnvironmentDialog.Dep.Column.Target=Destino +HopEnvironmentDialog.Dep.Edit.Add=Añadir dependencia +HopEnvironmentDialog.Dep.Edit.Edit=Editar dependencia +HopEnvironmentDialog.Dirty.Header=Cambios sin guardar +HopEnvironmentDialog.Dirty.Message=¿Guardar los cambios del archivo de entorno antes de continuar? +HopEnvironmentDialog.SaveFirst.Header=¿Guardar archivo de entorno? +HopEnvironmentDialog.SaveFirst.Message=¿Guardar el archivo de entorno antes de Validar o Aplicar? +HopEnvironmentDialog.Validation.Header=Entorno no válido +HopEnvironmentDialog.Validation.RepoIdUrl=Cada repositorio necesita un Id y una URL. +HopEnvironmentDialog.Validation.PluginArtifact=Cada plugin necesita un Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Cada dependencia necesita Group ID, Artifact ID y Version. +HopEnvironmentDialog.ImportRepos.Header=Importar repositorios +HopEnvironmentDialog.ImportRepos.Message=¿Reemplazar la lista de repositorios con los repositorios habilitados de hop-config.json? +HopEnvironmentDialog.Error.Header=Error del editor de entorno +HopEnvironmentDialog.Error.Load=No se pudo cargar el archivo de entorno:\n{0} +HopEnvironmentDialog.Error.Save=No se pudo guardar el archivo de entorno:\n{0} +HopEnvironmentDialog.Error.ImportRepos=No se pudieron importar repositorios desde hop-config +AddPluginsFromCatalogDialog.Shell.Title=Añadir plugins del catálogo +AddPluginsFromCatalogDialog.Filter.Label=Filtro +AddPluginsFromCatalogDialog.Button.Add=Añadir seleccionados +AddPluginsFromCatalogDialog.Button.Cancel=Cancelar +AddPluginsFromCatalogDialog.Column.Name=Nombre +AddPluginsFromCatalogDialog.Column.Artifact=Artefacto +AddPluginsFromCatalogDialog.Column.Category=Categoría +AddPluginsFromCatalogDialog.Column.Description=Descripción diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_fr_FR.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_fr_FR.properties new file mode 100644 index 00000000000..6379dfc1546 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_fr_FR.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugins Marketplace +MarketplaceLifecycleEnv.Help=L'ensemble des plugins est défini dans un fichier hop-env. Ces options contrôlent quand Hop vérifie ce fichier pour cet environnement de cycle de vie (souple en dev ; strict en production). +MarketplaceLifecycleEnv.EnvFile.Label=Fichier d'environnement +MarketplaceLifecycleEnv.Button.Browse=Parcourir… +MarketplaceLifecycleEnv.Button.EditEnv=Modifier… +MarketplaceLifecycleEnv.OnEnable.Label=À l'activation +MarketplaceLifecycleEnv.OnEnable.Tooltip=off : pas de contrôle ; warn : journal/dialogue en cas d'écart ; enforce : échouer à l'activation en cas d'écart +MarketplaceLifecycleEnv.Strict.Label=Strict (échouer si des plugins marketplace supplémentaires sont installés) +MarketplaceLifecycleEnv.Strict.Tooltip=Lors de la validation, traiter aussi comme écart les plugins marketplace avec reçu absents du fichier d'environnement +MarketplaceLifecycleEnv.AutoApply.Label=Appliquer automatiquement les plugins manquants à l'activation +MarketplaceLifecycleEnv.AutoApply.Tooltip=Si activé, exécute marketplace apply pour les plugins manquants (uniquement sur machines contrôlées ; désactivé par défaut) +MarketplaceDialog.Shell.Title=Marketplace de plugins +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Environnement +MarketplaceDialog.Tab.Repositories=Dépôts +MarketplaceDialog.Repository.Label=Dépôt principal +MarketplaceDialog.Repository.SetPrimary.Error=Impossible de définir le dépôt principal +MarketplaceDialog.Column.Name=Nom +MarketplaceDialog.Column.Artifact=Artefact +MarketplaceDialog.Column.Category=Catégorie +MarketplaceDialog.Column.Status=Statut +MarketplaceDialog.Column.Description=Description +MarketplaceDialog.Search.Label=Rechercher +MarketplaceDialog.Search.Placeholder=Saisissez 3 caractères ou plus pour filtrer les plugins… +MarketplaceDialog.Search.Clear=Effacer le filtre de recherche +MarketplaceDialog.Button.Install=Installer +MarketplaceDialog.Button.Uninstall=Désinstaller +MarketplaceDialog.Button.Refresh=Actualiser +MarketplaceDialog.Button.Close=Fermer +MarketplaceDialog.Button.ManageRepos=Gérer… +MarketplaceDialog.Button.BrowseEnv=Parcourir… +MarketplaceDialog.Button.EditEnv=Modifier… +MarketplaceDialog.Button.EditEnv.Tooltip=Créer ou modifier le fichier d'environnement (dépôts, plugins, dépendances) +MarketplaceDialog.Button.ValidateEnv=Valider +MarketplaceDialog.Button.ValidateEnv.Tooltip=Comparer l'installation locale au fichier d'environnement sans installer +MarketplaceDialog.Button.ApplyEnv=Appliquer +MarketplaceDialog.Button.ApplyEnv.Tooltip=Installer les plugins et dépendances déclarés dans le fichier d'environnement +MarketplaceDialog.EnvFile.Label=Fichier d'environnement +MarketplaceDialog.EnvFile.Prune=Supprimer les extras à l'application +MarketplaceDialog.EnvFile.Prune.Tooltip=Lors de l'application, désinstaller les plugins marketplace avec reçu absents du fichier +MarketplaceDialog.EnvFile.Strict=Validation stricte +MarketplaceDialog.EnvFile.Strict.Tooltip=Lors de la validation, échouer aussi si des plugins marketplace supplémentaires sont installés au-delà du fichier +MarketplaceDialog.EnvFile.Filter.Env=Fichiers d'environnement Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Tous les fichiers +MarketplaceDialog.EnvFile.Missing.Header=Fichier d'environnement requis +MarketplaceDialog.EnvFile.Missing.Message=Sélectionnez un hop-env.yaml (ou full-client-env.yaml) avec Parcourir…, ou saisissez le chemin. +MarketplaceDialog.EnvFile.NotFound.Header=Fichier introuvable +MarketplaceDialog.EnvFile.NotFound.Message=Le fichier d''environnement n''existe pas :\n{0} +MarketplaceDialog.Status.Installed=Installé +MarketplaceDialog.Status.Present=Présent sur le disque +MarketplaceDialog.Status.NotInstalled=Non installé +MarketplaceDialog.Status.RestartHint=Redémarrez Hop après installation, application ou désinstallation pour recharger les plugins. L'installation tente d'abord le dépôt principal, puis les autres dépôts activés. +MarketplaceDialog.Status.Installing=Installation de {0}... +MarketplaceDialog.Status.Validating=Validation du fichier d'environnement... +MarketplaceDialog.Status.Applying=Application du fichier d'environnement... +MarketplaceDialog.Status.Drift=Écart d'environnement détecté — utilisez Appliquer pour corriger +MarketplaceDialog.Error.Header=Erreur Marketplace +MarketplaceDialog.Error.ResolveHome=Impossible de déterminer le répertoire d'installation de Hop. Démarrez Hop depuis la racine d'installation avec le lanceur Hop. +MarketplaceDialog.Install.Done.Header=Plugin installé +MarketplaceDialog.Install.Done.Message=Installé : {0}.\n\nVeuillez redémarrer Hop pour charger le plugin. +MarketplaceDialog.Install.Error=Échec de l''installation de {0} +MarketplaceDialog.Uninstall.Confirm.Header=Désinstaller le plugin +MarketplaceDialog.Uninstall.Confirm.Message=Supprimer le plugin installé via le marketplace {0} ? +MarketplaceDialog.Uninstall.NoReceipt.Header=Désinstallation impossible +MarketplaceDialog.Uninstall.NoReceipt.Message=Aucun reçu d''installation marketplace pour {0}. Seuls les plugins installés via le marketplace peuvent être désinstallés ici. +MarketplaceDialog.Uninstall.Error=Échec de la désinstallation de {0} +MarketplaceDialog.Validate.Ok.Header=Environnement OK +MarketplaceDialog.Validate.Ok.Message=L''installation locale correspond :\n{0} +MarketplaceDialog.Validate.Drift.Header=Écart d'environnement +MarketplaceDialog.Validate.Drift.Message=L'installation locale ne correspond pas au fichier d'environnement : +MarketplaceDialog.Validate.Drift.Hint=Utilisez Appliquer pour installer les plugins manquants (ou corriger les versions), puis redémarrez Hop. +MarketplaceDialog.Validate.Error=Échec de la validation du fichier d'environnement +MarketplaceDialog.Apply.PruneConfirm.Header=Élaguer les plugins marketplace ? +MarketplaceDialog.Apply.PruneConfirm.Message=Appliquer avec élagage désinstallera les plugins marketplace absents du fichier d'environnement.\n\nContinuer ? +MarketplaceDialog.Apply.Disabled=Le marketplace est désactivé dans hop-config.json. +MarketplaceDialog.Apply.Done.Header=Environnement appliqué +MarketplaceDialog.Apply.Done.Message=Appliqué :\n{0}\n\nVeuillez redémarrer Hop pour charger les nouveaux plugins. +MarketplaceDialog.Apply.Error=Échec de l'application du fichier d'environnement +ManageRepositoriesDialog.Shell.Title=Dépôts Marketplace +ManageRepositoriesDialog.Help=Le dépôt principal est essayé en premier. Les autres dépôts activés sont des bascules.\nPar défaut : Apache Repository (repository.apache.org) puis Maven Central.\nLes paramètres sont enregistrés dans hop-config.json sous '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principal +ManageRepositoriesDialog.Column.Enabled=Activé +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nom +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Ajouter +ManageRepositoriesDialog.Button.Edit=Modifier +ManageRepositoriesDialog.Button.Remove=Supprimer +ManageRepositoriesDialog.Button.SetPrimary=Définir comme principal +ManageRepositoriesDialog.Button.MoveUp=Haut +ManageRepositoriesDialog.Button.MoveDown=Bas +ManageRepositoriesDialog.Button.ResetDefaults=Réinitialiser les valeurs par défaut +ManageRepositoriesDialog.Button.Save=Enregistrer +ManageRepositoriesDialog.Button.Cancel=Annuler +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Dépôts enregistrés +ManageRepositoriesDialog.Save.Done.Message=Les paramètres des dépôts Marketplace ont été enregistrés dans hop-config.json. +ManageRepositoriesDialog.Error.Header=Erreur de dépôt +ManageRepositoriesDialog.Error.Save=Impossible d'enregistrer la configuration Marketplace +ManageRepositoriesDialog.Reset.Header=Réinitialiser les dépôts +ManageRepositoriesDialog.Reset.Message=Remplacer la liste des dépôts par Apache Repository (principal) et Maven Central ? +ManageRepositoriesDialog.Edit.Title.Add=Ajouter un dépôt +ManageRepositoriesDialog.Edit.Title.Edit=Modifier le dépôt +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nom +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Nom d'utilisateur +ManageRepositoriesDialog.Edit.Password=Mot de passe +ManageRepositoriesDialog.Edit.Enabled=Activé (utiliser dans la chaîne de bascule) +ManageRepositoriesDialog.Edit.Primary=Principal (essayé en premier) +ManageRepositoriesDialog.Edit.IdUrlRequired=L'Id et l'URL sont obligatoires. +HopEnvironmentDialog.Shell.Title=Environnement Marketplace — {0} +HopEnvironmentDialog.Untitled=Sans titre +HopEnvironmentDialog.Button.New=Nouveau +HopEnvironmentDialog.Button.Open=Ouvrir… +HopEnvironmentDialog.Button.Save=Enregistrer +HopEnvironmentDialog.Button.SaveAs=Enregistrer sous… +HopEnvironmentDialog.Button.Validate=Valider +HopEnvironmentDialog.Button.Validate.Tooltip=Comparer l'installation locale à ce fichier d'environnement sans installer +HopEnvironmentDialog.Button.Apply=Appliquer +HopEnvironmentDialog.Button.Apply.Tooltip=Installer les plugins et dépendances déclarés dans ce fichier d'environnement +HopEnvironmentDialog.Button.Close=Fermer +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Annuler +HopEnvironmentDialog.Button.Add=Ajouter +HopEnvironmentDialog.Button.Edit=Modifier +HopEnvironmentDialog.Button.Remove=Supprimer +HopEnvironmentDialog.Button.MoveUp=Haut +HopEnvironmentDialog.Button.MoveDown=Bas +HopEnvironmentDialog.Button.AddFromCatalog=Depuis le catalogue… +HopEnvironmentDialog.Button.ImportRepos=Importer depuis hop-config… +HopEnvironmentDialog.Prune=Supprimer les extras à l'application +HopEnvironmentDialog.Prune.Tooltip=Lors de l'application, désinstaller les plugins marketplace avec reçu absents du fichier +HopEnvironmentDialog.Strict=Validation stricte +HopEnvironmentDialog.Strict.Tooltip=Lors de la validation, échouer aussi si des plugins marketplace supplémentaires sont installés au-delà du fichier +HopEnvironmentDialog.Filter.Yaml=Environnement YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Environnement JSON (*.json) +HopEnvironmentDialog.Tab.General=Général +HopEnvironmentDialog.Tab.Repositories=Dépôts +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dépendances +HopEnvironmentDialog.General.Help=Déclarez quels plugins et jars cette installation Hop doit avoir. Enregistrez en hop-env.yaml pour CI/Docker, ou utilisez Valider / Appliquer dans ce dialogue. +HopEnvironmentDialog.General.Version=Version de spécification +HopEnvironmentDialog.General.HopVersion=Version Hop +HopEnvironmentDialog.General.EnforceOnRun=Appliquer sur hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Si vrai, hop-run peut échouer si l'installation s'écarte de ce fichier (aussi contrôlé par -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=La première ligne est principale (essayée en premier) ; les suivantes sont des bascules. Ces dépôts sont dans le fichier d'environnement (pas hop-config). Évitez de versionner les mots de passe. +HopEnvironmentDialog.Plugins.Help=Plugins marketplace à installer. Laissez Group ID vide pour org.apache.hop. La version peut provenir de la version Hop de l'onglet Général à l'application. +HopEnvironmentDialog.Dependencies.Help=Jars Maven optionnels (par ex. pilotes JDBC) installés sous le répertoire cible de Hop home (par défaut lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Nom d'utilisateur +HopEnvironmentDialog.Repo.Column.Password=Mot de passe +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Ajouter un dépôt +HopEnvironmentDialog.Repo.Edit.Edit=Modifier le dépôt +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Version +HopEnvironmentDialog.Plugin.Edit.Add=Ajouter un plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Modifier le plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Version +HopEnvironmentDialog.Dep.Column.Target=Cible +HopEnvironmentDialog.Dep.Edit.Add=Ajouter une dépendance +HopEnvironmentDialog.Dep.Edit.Edit=Modifier la dépendance +HopEnvironmentDialog.Dirty.Header=Modifications non enregistrées +HopEnvironmentDialog.Dirty.Message=Enregistrer les modifications du fichier d'environnement avant de continuer ? +HopEnvironmentDialog.SaveFirst.Header=Enregistrer le fichier d'environnement ? +HopEnvironmentDialog.SaveFirst.Message=Enregistrer le fichier d'environnement avant Valider ou Appliquer ? +HopEnvironmentDialog.Validation.Header=Environnement non valide +HopEnvironmentDialog.Validation.RepoIdUrl=Chaque dépôt doit avoir un Id et une URL. +HopEnvironmentDialog.Validation.PluginArtifact=Chaque plugin doit avoir un Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Chaque dépendance doit avoir Group ID, Artifact ID et Version. +HopEnvironmentDialog.ImportRepos.Header=Importer les dépôts +HopEnvironmentDialog.ImportRepos.Message=Remplacer la liste des dépôts par les dépôts activés de hop-config.json ? +HopEnvironmentDialog.Error.Header=Erreur de l'éditeur d'environnement +HopEnvironmentDialog.Error.Load=Impossible de charger le fichier d''environnement :\n{0} +HopEnvironmentDialog.Error.Save=Impossible d''enregistrer le fichier d''environnement :\n{0} +HopEnvironmentDialog.Error.ImportRepos=Impossible d'importer les dépôts depuis hop-config +AddPluginsFromCatalogDialog.Shell.Title=Ajouter des plugins depuis le catalogue +AddPluginsFromCatalogDialog.Filter.Label=Filtre +AddPluginsFromCatalogDialog.Button.Add=Ajouter la sélection +AddPluginsFromCatalogDialog.Button.Cancel=Annuler +AddPluginsFromCatalogDialog.Column.Name=Nom +AddPluginsFromCatalogDialog.Column.Artifact=Artefact +AddPluginsFromCatalogDialog.Column.Category=Catégorie +AddPluginsFromCatalogDialog.Column.Description=Description diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_it_IT.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_it_IT.properties new file mode 100644 index 00000000000..2507bc7af71 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_it_IT.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugin Marketplace +MarketplaceLifecycleEnv.Help=Il set di plugin è definito in un file hop-env. Queste opzioni controllano quando Hop verifica quel file per questo ambiente di lifecycle (flessibile in dev; rigoroso in produzione). +MarketplaceLifecycleEnv.EnvFile.Label=File ambiente +MarketplaceLifecycleEnv.Button.Browse=Sfoglia… +MarketplaceLifecycleEnv.Button.EditEnv=Modifica… +MarketplaceLifecycleEnv.OnEnable.Label=All'abilitazione +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: nessun controllo; warn: log/dialogo in caso di scostamento; enforce: fallire l'abilitazione dell'ambiente in caso di scostamento +MarketplaceLifecycleEnv.Strict.Label=Rigoroso (fallire se sono installati plugin marketplace extra) +MarketplaceLifecycleEnv.Strict.Tooltip=In validazione, considera anche come scostamento i plugin marketplace con ricevuta non elencati nel file ambiente +MarketplaceLifecycleEnv.AutoApply.Label=Applica automaticamente i plugin mancanti all'abilitazione +MarketplaceLifecycleEnv.AutoApply.Tooltip=Se abilitato, esegue marketplace apply per i plugin mancanti (solo su macchine controllate; predefinito off) +MarketplaceDialog.Shell.Title=Marketplace dei plugin +MarketplaceDialog.Tab.Plugins=Plugin +MarketplaceDialog.Tab.Environment=Ambiente +MarketplaceDialog.Tab.Repositories=Repository +MarketplaceDialog.Repository.Label=Repository principale +MarketplaceDialog.Repository.SetPrimary.Error=Impossibile impostare il repository principale +MarketplaceDialog.Column.Name=Nome +MarketplaceDialog.Column.Artifact=Artifact +MarketplaceDialog.Column.Category=Categoria +MarketplaceDialog.Column.Status=Stato +MarketplaceDialog.Column.Description=Descrizione +MarketplaceDialog.Search.Label=Cerca +MarketplaceDialog.Search.Placeholder=Digitare 3+ caratteri per filtrare i plugin… +MarketplaceDialog.Search.Clear=Cancella filtro di ricerca +MarketplaceDialog.Button.Install=Installa +MarketplaceDialog.Button.Uninstall=Disinstalla +MarketplaceDialog.Button.Refresh=Aggiorna +MarketplaceDialog.Button.Close=Chiudi +MarketplaceDialog.Button.ManageRepos=Gestisci… +MarketplaceDialog.Button.BrowseEnv=Sfoglia… +MarketplaceDialog.Button.EditEnv=Modifica… +MarketplaceDialog.Button.EditEnv.Tooltip=Crea o modifica il file ambiente (repository, plugin, dipendenze) +MarketplaceDialog.Button.ValidateEnv=Valida +MarketplaceDialog.Button.ValidateEnv.Tooltip=Confronta l'installazione locale con il file ambiente senza installare +MarketplaceDialog.Button.ApplyEnv=Applica +MarketplaceDialog.Button.ApplyEnv.Tooltip=Installa plugin e dipendenze dichiarati nel file ambiente +MarketplaceDialog.EnvFile.Label=File ambiente +MarketplaceDialog.EnvFile.Prune=Rimuovi extra all'applicazione +MarketplaceDialog.EnvFile.Prune.Tooltip=All'applicazione, disinstalla i plugin marketplace con ricevuta non elencati nel file +MarketplaceDialog.EnvFile.Strict=Validazione rigorosa +MarketplaceDialog.EnvFile.Strict.Tooltip=In validazione, fallire anche se sono installati plugin marketplace extra oltre al file +MarketplaceDialog.EnvFile.Filter.Env=File ambiente Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Tutti i file +MarketplaceDialog.EnvFile.Missing.Header=File ambiente richiesto +MarketplaceDialog.EnvFile.Missing.Message=Selezionare un hop-env.yaml (o full-client-env.yaml) con Sfoglia…, oppure digitare il percorso. +MarketplaceDialog.EnvFile.NotFound.Header=File non trovato +MarketplaceDialog.EnvFile.NotFound.Message=Il file ambiente non esiste:\n{0} +MarketplaceDialog.Status.Installed=Installato +MarketplaceDialog.Status.Present=Presente su disco +MarketplaceDialog.Status.NotInstalled=Non installato +MarketplaceDialog.Status.RestartHint=Riavviare Hop dopo installazione, applicazione o disinstallazione per ricaricare i plugin. L'installazione prova prima il repository principale, poi gli altri abilitati. +MarketplaceDialog.Status.Installing=Installazione di {0}... +MarketplaceDialog.Status.Validating=Validazione del file ambiente... +MarketplaceDialog.Status.Applying=Applicazione del file ambiente... +MarketplaceDialog.Status.Drift=Scostamento ambiente rilevato — usare Applica per correggere +MarketplaceDialog.Error.Header=Errore Marketplace +MarketplaceDialog.Error.ResolveHome=Impossibile determinare la directory di installazione di Hop. Avviare Hop dalla root di installazione con il launcher Hop. +MarketplaceDialog.Install.Done.Header=Plugin installato +MarketplaceDialog.Install.Done.Message=Installato {0}.\n\nRiavviare Hop per caricare il plugin. +MarketplaceDialog.Install.Error=Installazione di {0} non riuscita +MarketplaceDialog.Uninstall.Confirm.Header=Disinstalla plugin +MarketplaceDialog.Uninstall.Confirm.Message=Rimuovere il plugin installato dal marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Impossibile disinstallare +MarketplaceDialog.Uninstall.NoReceipt.Message=Nessuna ricevuta di installazione marketplace per {0}. Qui si possono disinstallare solo i plugin installati tramite marketplace. +MarketplaceDialog.Uninstall.Error=Disinstallazione di {0} non riuscita +MarketplaceDialog.Validate.Ok.Header=Ambiente OK +MarketplaceDialog.Validate.Ok.Message=L''installazione locale corrisponde:\n{0} +MarketplaceDialog.Validate.Drift.Header=Scostamento ambiente +MarketplaceDialog.Validate.Drift.Message=L'installazione locale non corrisponde al file ambiente: +MarketplaceDialog.Validate.Drift.Hint=Usare Applica per installare i plugin mancanti (o correggere le versioni), poi riavviare Hop. +MarketplaceDialog.Validate.Error=Validazione del file ambiente non riuscita +MarketplaceDialog.Apply.PruneConfirm.Header=Rimuovere plugin marketplace extra? +MarketplaceDialog.Apply.PruneConfirm.Message=Applica con rimozione extra disinstallerà i plugin marketplace non elencati nel file ambiente.\n\nContinuare? +MarketplaceDialog.Apply.Disabled=Il marketplace è disabilitato in hop-config.json. +MarketplaceDialog.Apply.Done.Header=Ambiente applicato +MarketplaceDialog.Apply.Done.Message=Applicato:\n{0}\n\nRiavviare Hop per caricare i nuovi plugin. +MarketplaceDialog.Apply.Error=Applicazione del file ambiente non riuscita +ManageRepositoriesDialog.Shell.Title=Repository Marketplace +ManageRepositoriesDialog.Help=Il repository principale viene provato per primo. Gli altri repository abilitati sono fallback.\nPredefiniti: Apache Repository (repository.apache.org) poi Maven Central.\nLe impostazioni sono salvate in hop-config.json sotto '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principale +ManageRepositoriesDialog.Column.Enabled=Abilitato +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nome +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Aggiungi +ManageRepositoriesDialog.Button.Edit=Modifica +ManageRepositoriesDialog.Button.Remove=Rimuovi +ManageRepositoriesDialog.Button.SetPrimary=Imposta principale +ManageRepositoriesDialog.Button.MoveUp=Su +ManageRepositoriesDialog.Button.MoveDown=Giù +ManageRepositoriesDialog.Button.ResetDefaults=Ripristina predefiniti +ManageRepositoriesDialog.Button.Save=Salva +ManageRepositoriesDialog.Button.Cancel=Annulla +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repository salvati +ManageRepositoriesDialog.Save.Done.Message=Le impostazioni dei repository Marketplace sono state salvate in hop-config.json. +ManageRepositoriesDialog.Error.Header=Errore repository +ManageRepositoriesDialog.Error.Save=Impossibile salvare la configurazione Marketplace +ManageRepositoriesDialog.Reset.Header=Ripristina repository +ManageRepositoriesDialog.Reset.Message=Sostituire l'elenco dei repository con Apache Repository (principale) e Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Aggiungi repository +ManageRepositoriesDialog.Edit.Title.Edit=Modifica repository +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nome +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Nome utente +ManageRepositoriesDialog.Edit.Password=Password +ManageRepositoriesDialog.Edit.Enabled=Abilitato (usa nella catena di fallback) +ManageRepositoriesDialog.Edit.Primary=Principale (provato per primo) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id e URL sono obbligatori. +HopEnvironmentDialog.Shell.Title=Ambiente Marketplace — {0} +HopEnvironmentDialog.Untitled=Senza titolo +HopEnvironmentDialog.Button.New=Nuovo +HopEnvironmentDialog.Button.Open=Apri… +HopEnvironmentDialog.Button.Save=Salva +HopEnvironmentDialog.Button.SaveAs=Salva con nome… +HopEnvironmentDialog.Button.Validate=Valida +HopEnvironmentDialog.Button.Validate.Tooltip=Confronta l'installazione locale con questo file ambiente senza installare +HopEnvironmentDialog.Button.Apply=Applica +HopEnvironmentDialog.Button.Apply.Tooltip=Installa plugin e dipendenze dichiarati in questo file ambiente +HopEnvironmentDialog.Button.Close=Chiudi +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Annulla +HopEnvironmentDialog.Button.Add=Aggiungi +HopEnvironmentDialog.Button.Edit=Modifica +HopEnvironmentDialog.Button.Remove=Rimuovi +HopEnvironmentDialog.Button.MoveUp=Su +HopEnvironmentDialog.Button.MoveDown=Giù +HopEnvironmentDialog.Button.AddFromCatalog=Dal catalogo… +HopEnvironmentDialog.Button.ImportRepos=Importa da hop-config… +HopEnvironmentDialog.Prune=Rimuovi extra all'applicazione +HopEnvironmentDialog.Prune.Tooltip=All'applicazione, disinstalla i plugin marketplace con ricevuta non elencati nel file +HopEnvironmentDialog.Strict=Validazione rigorosa +HopEnvironmentDialog.Strict.Tooltip=In validazione, fallire anche se sono installati plugin marketplace extra oltre al file +HopEnvironmentDialog.Filter.Yaml=Ambiente YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Ambiente JSON (*.json) +HopEnvironmentDialog.Tab.General=Generale +HopEnvironmentDialog.Tab.Repositories=Repository +HopEnvironmentDialog.Tab.Plugins=Plugin +HopEnvironmentDialog.Tab.Dependencies=Dipendenze +HopEnvironmentDialog.General.Help=Dichiarare quali plugin e jar deve avere questa installazione Hop. Salvare come hop-env.yaml per CI/Docker, oppure usare Valida / Applica in questa finestra. +HopEnvironmentDialog.General.Version=Versione specifica +HopEnvironmentDialog.General.HopVersion=Versione Hop +HopEnvironmentDialog.General.EnforceOnRun=Applica su hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Se true, hop-run può fallire se l'installazione si discosta da questo file (anche controllato da -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=La prima riga è principale (provata per prima); le successive sono fallback. Questi repository sono nel file ambiente (non hop-config). Preferire non committare le password. +HopEnvironmentDialog.Plugins.Help=Plugin marketplace da installare. Lasciare Group ID vuoto per usare org.apache.hop. La versione può derivare dalla versione Hop nella scheda Generale all'applicazione. +HopEnvironmentDialog.Dependencies.Help=Jar Maven opzionali (ad es. driver JDBC) installati sotto la directory di destinazione di Hop home (predefinito lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Nome utente +HopEnvironmentDialog.Repo.Column.Password=Password +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Aggiungi repository +HopEnvironmentDialog.Repo.Edit.Edit=Modifica repository +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versione +HopEnvironmentDialog.Plugin.Edit.Add=Aggiungi plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Modifica plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versione +HopEnvironmentDialog.Dep.Column.Target=Destinazione +HopEnvironmentDialog.Dep.Edit.Add=Aggiungi dipendenza +HopEnvironmentDialog.Dep.Edit.Edit=Modifica dipendenza +HopEnvironmentDialog.Dirty.Header=Modifiche non salvate +HopEnvironmentDialog.Dirty.Message=Salvare le modifiche al file ambiente prima di continuare? +HopEnvironmentDialog.SaveFirst.Header=Salvare il file ambiente? +HopEnvironmentDialog.SaveFirst.Message=Salvare il file ambiente prima di Valida o Applica? +HopEnvironmentDialog.Validation.Header=Ambiente non valido +HopEnvironmentDialog.Validation.RepoIdUrl=Ogni repository richiede un Id e un URL. +HopEnvironmentDialog.Validation.PluginArtifact=Ogni plugin richiede un Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Ogni dipendenza richiede Group ID, Artifact ID e Version. +HopEnvironmentDialog.ImportRepos.Header=Importa repository +HopEnvironmentDialog.ImportRepos.Message=Sostituire l'elenco dei repository con i repository abilitati da hop-config.json? +HopEnvironmentDialog.Error.Header=Errore dell'editor ambiente +HopEnvironmentDialog.Error.Load=Impossibile caricare il file ambiente:\n{0} +HopEnvironmentDialog.Error.Save=Impossibile salvare il file ambiente:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Impossibile importare i repository da hop-config +AddPluginsFromCatalogDialog.Shell.Title=Aggiungi plugin dal catalogo +AddPluginsFromCatalogDialog.Filter.Label=Filtro +AddPluginsFromCatalogDialog.Button.Add=Aggiungi selezionati +AddPluginsFromCatalogDialog.Button.Cancel=Annulla +AddPluginsFromCatalogDialog.Column.Name=Nome +AddPluginsFromCatalogDialog.Column.Artifact=Artifact +AddPluginsFromCatalogDialog.Column.Category=Categoria +AddPluginsFromCatalogDialog.Column.Description=Descrizione diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ja_JP.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ja_JP.properties new file mode 100644 index 00000000000..a8435bc32e8 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ja_JP.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplaceプラグイン +MarketplaceLifecycleEnv.Help=プラグインのセットは hop-env ファイルで定義します。これらのオプションは、このライフサイクル環境で Hop がそのファイルをいつ確認するかを制御します(開発は緩く、本番は厳格に)。 +MarketplaceLifecycleEnv.EnvFile.Label=環境ファイル +MarketplaceLifecycleEnv.Button.Browse=参照… +MarketplaceLifecycleEnv.Button.EditEnv=編集… +MarketplaceLifecycleEnv.OnEnable.Label=有効化時 +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: チェックなし; warn: 差分時にログ/ダイアログ; enforce: 差分時に環境の有効化を失敗させる +MarketplaceLifecycleEnv.Strict.Label=厳密(追加の Marketplace プラグインがインストールされている場合は失敗) +MarketplaceLifecycleEnv.Strict.Tooltip=検証時、環境ファイルにないレシート付き Marketplace プラグインも差分として扱います +MarketplaceLifecycleEnv.AutoApply.Label=有効化時に不足プラグインを自動適用 +MarketplaceLifecycleEnv.AutoApply.Tooltip=有効にすると、不足プラグインに対して marketplace apply を実行します(管理されたマシンのみ;既定はオフ) +MarketplaceDialog.Shell.Title=プラグイン Marketplace +MarketplaceDialog.Tab.Plugins=プラグイン +MarketplaceDialog.Tab.Environment=環境 +MarketplaceDialog.Tab.Repositories=リポジトリ +MarketplaceDialog.Repository.Label=プライマリリポジトリ +MarketplaceDialog.Repository.SetPrimary.Error=プライマリリポジトリを設定できませんでした +MarketplaceDialog.Column.Name=名前 +MarketplaceDialog.Column.Artifact=アーティファクト +MarketplaceDialog.Column.Category=カテゴリ +MarketplaceDialog.Column.Status=状態 +MarketplaceDialog.Column.Description=説明 +MarketplaceDialog.Search.Label=検索 +MarketplaceDialog.Search.Placeholder=3文字以上入力してプラグインを絞り込み… +MarketplaceDialog.Search.Clear=検索フィルタをクリア +MarketplaceDialog.Button.Install=インストール +MarketplaceDialog.Button.Uninstall=アンインストール +MarketplaceDialog.Button.Refresh=更新 +MarketplaceDialog.Button.Close=閉じる +MarketplaceDialog.Button.ManageRepos=管理… +MarketplaceDialog.Button.BrowseEnv=参照… +MarketplaceDialog.Button.EditEnv=編集… +MarketplaceDialog.Button.EditEnv.Tooltip=環境ファイルの作成または編集(リポジトリ、プラグイン、依存関係) +MarketplaceDialog.Button.ValidateEnv=検証 +MarketplaceDialog.Button.ValidateEnv.Tooltip=インストールせずにローカル環境と環境ファイルを照合します +MarketplaceDialog.Button.ApplyEnv=適用 +MarketplaceDialog.Button.ApplyEnv.Tooltip=環境ファイルで宣言されたプラグインと依存関係をインストールします +MarketplaceDialog.EnvFile.Label=環境ファイル +MarketplaceDialog.EnvFile.Prune=適用時に余分なものを削除 +MarketplaceDialog.EnvFile.Prune.Tooltip=適用時、ファイルにないレシート付き Marketplace プラグインをアンインストールします +MarketplaceDialog.EnvFile.Strict=厳密な検証 +MarketplaceDialog.EnvFile.Strict.Tooltip=検証時、ファイル以外の追加 Marketplace プラグインがインストールされている場合も失敗します +MarketplaceDialog.EnvFile.Filter.Env=Hop 環境ファイル (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=すべてのファイル +MarketplaceDialog.EnvFile.Missing.Header=環境ファイルが必要です +MarketplaceDialog.EnvFile.Missing.Message=参照… で hop-env.yaml(または full-client-env.yaml)を選択するか、パスを入力してください。 +MarketplaceDialog.EnvFile.NotFound.Header=ファイルが見つかりません +MarketplaceDialog.EnvFile.NotFound.Message=環境ファイルが存在しません:\n{0} +MarketplaceDialog.Status.Installed=インストール済み +MarketplaceDialog.Status.Present=ディスク上に存在 +MarketplaceDialog.Status.NotInstalled=未インストール +MarketplaceDialog.Status.RestartHint=インストール、適用、アンインストール後はプラグイン再読込のため Hop を再起動してください。インストールはプライマリリポジトリ、次いで他の有効なリポジトリを試します。 +MarketplaceDialog.Status.Installing={0} をインストール中... +MarketplaceDialog.Status.Validating=環境ファイルを検証中... +MarketplaceDialog.Status.Applying=環境ファイルを適用中... +MarketplaceDialog.Status.Drift=環境の差分を検出 — 適用で修正してください +MarketplaceDialog.Error.Header=Marketplace エラー +MarketplaceDialog.Error.ResolveHome=Hop のインストールディレクトリを特定できませんでした。インストールルートから hop ランチャーで起動してください。 +MarketplaceDialog.Install.Done.Header=プラグインをインストールしました +MarketplaceDialog.Install.Done.Message={0} をインストールしました。\n\nプラグインを読み込むには Hop を再起動してください。 +MarketplaceDialog.Install.Error={0} のインストールに失敗しました +MarketplaceDialog.Uninstall.Confirm.Header=プラグインのアンインストール +MarketplaceDialog.Uninstall.Confirm.Message=Marketplace でインストールしたプラグイン {0} を削除しますか? +MarketplaceDialog.Uninstall.NoReceipt.Header=アンインストールできません +MarketplaceDialog.Uninstall.NoReceipt.Message={0} の Marketplace インストールレシートがありません。ここでアンインストールできるのは Marketplace 経由のプラグインのみです。 +MarketplaceDialog.Uninstall.Error={0} のアンインストールに失敗しました +MarketplaceDialog.Validate.Ok.Header=環境 OK +MarketplaceDialog.Validate.Ok.Message=ローカルインストールは一致しています:\n{0} +MarketplaceDialog.Validate.Drift.Header=環境の差分 +MarketplaceDialog.Validate.Drift.Message=ローカルインストールが環境ファイルと一致しません: +MarketplaceDialog.Validate.Drift.Hint=適用で不足プラグインをインストール(またはバージョンを修正)し、Hop を再起動してください。 +MarketplaceDialog.Validate.Error=環境ファイルの検証に失敗しました +MarketplaceDialog.Apply.PruneConfirm.Header=Marketplace プラグインを整理しますか? +MarketplaceDialog.Apply.PruneConfirm.Message=整理付きの適用は、環境ファイルにない Marketplace プラグインをアンインストールします。\n\n続行しますか? +MarketplaceDialog.Apply.Disabled=Marketplace は hop-config.json で無効です。 +MarketplaceDialog.Apply.Done.Header=環境を適用しました +MarketplaceDialog.Apply.Done.Message=適用しました:\n{0}\n\n新しいプラグインを読み込むには Hop を再起動してください。 +MarketplaceDialog.Apply.Error=環境ファイルの適用に失敗しました +ManageRepositoriesDialog.Shell.Title=Marketplace リポジトリ +ManageRepositoriesDialog.Help=プライマリリポジトリが最初に試されます。他の有効なリポジトリはフォールバックです。\n既定: Apache Repository (repository.apache.org)、次に Maven Central。\n設定は '${HOP_CONFIG_FOLDER}' の hop-config.json に保存されます。 +ManageRepositoriesDialog.Column.Primary=プライマリ +ManageRepositoriesDialog.Column.Enabled=有効 +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=名前 +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=認証 +ManageRepositoriesDialog.Button.Add=追加 +ManageRepositoriesDialog.Button.Edit=編集 +ManageRepositoriesDialog.Button.Remove=削除 +ManageRepositoriesDialog.Button.SetPrimary=プライマリに設定 +ManageRepositoriesDialog.Button.MoveUp=上へ +ManageRepositoriesDialog.Button.MoveDown=下へ +ManageRepositoriesDialog.Button.ResetDefaults=既定にリセット +ManageRepositoriesDialog.Button.Save=保存 +ManageRepositoriesDialog.Button.Cancel=キャンセル +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=リポジトリを保存しました +ManageRepositoriesDialog.Save.Done.Message=Marketplace リポジトリ設定を hop-config.json に保存しました。 +ManageRepositoriesDialog.Error.Header=リポジトリエラー +ManageRepositoriesDialog.Error.Save=Marketplace 設定を保存できませんでした +ManageRepositoriesDialog.Reset.Header=リポジトリをリセット +ManageRepositoriesDialog.Reset.Message=リポジトリ一覧を Apache Repository(プライマリ)と Maven Central に置き換えますか? +ManageRepositoriesDialog.Edit.Title.Add=リポジトリを追加 +ManageRepositoriesDialog.Edit.Title.Edit=リポジトリを編集 +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=名前 +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=ユーザー名 +ManageRepositoriesDialog.Edit.Password=パスワード +ManageRepositoriesDialog.Edit.Enabled=有効(フォールバックチェーンで使用) +ManageRepositoriesDialog.Edit.Primary=プライマリ(最初に試行) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id と URL は必須です。 +HopEnvironmentDialog.Shell.Title=Marketplace 環境 — {0} +HopEnvironmentDialog.Untitled=無題 +HopEnvironmentDialog.Button.New=新規 +HopEnvironmentDialog.Button.Open=開く… +HopEnvironmentDialog.Button.Save=保存 +HopEnvironmentDialog.Button.SaveAs=名前を付けて保存… +HopEnvironmentDialog.Button.Validate=検証 +HopEnvironmentDialog.Button.Validate.Tooltip=インストールせずにローカル環境とこの環境ファイルを照合します +HopEnvironmentDialog.Button.Apply=適用 +HopEnvironmentDialog.Button.Apply.Tooltip=この環境ファイルで宣言されたプラグインと依存関係をインストールします +HopEnvironmentDialog.Button.Close=閉じる +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=キャンセル +HopEnvironmentDialog.Button.Add=追加 +HopEnvironmentDialog.Button.Edit=編集 +HopEnvironmentDialog.Button.Remove=削除 +HopEnvironmentDialog.Button.MoveUp=上へ +HopEnvironmentDialog.Button.MoveDown=下へ +HopEnvironmentDialog.Button.AddFromCatalog=カタログから… +HopEnvironmentDialog.Button.ImportRepos=hop-config からインポート… +HopEnvironmentDialog.Prune=適用時に余分なものを削除 +HopEnvironmentDialog.Prune.Tooltip=適用時、ファイルにないレシート付き Marketplace プラグインをアンインストールします +HopEnvironmentDialog.Strict=厳密な検証 +HopEnvironmentDialog.Strict.Tooltip=検証時、ファイル以外の追加 Marketplace プラグインがインストールされている場合も失敗します +HopEnvironmentDialog.Filter.Yaml=YAML 環境 (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON 環境 (*.json) +HopEnvironmentDialog.Tab.General=全般 +HopEnvironmentDialog.Tab.Repositories=リポジトリ +HopEnvironmentDialog.Tab.Plugins=プラグイン +HopEnvironmentDialog.Tab.Dependencies=依存関係 +HopEnvironmentDialog.General.Help=この Hop インストールに必要なプラグインと JAR を宣言します。CI/Docker 用に hop-env.yaml として保存するか、このダイアログで検証/適用を使用します。 +HopEnvironmentDialog.General.Version=仕様バージョン +HopEnvironmentDialog.General.HopVersion=Hop バージョン +HopEnvironmentDialog.General.EnforceOnRun=hop-run で強制 +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=true の場合、インストールがこのファイルから逸脱すると hop-run が失敗できます(-Dhop.env.enforce=true でも制御) +HopEnvironmentDialog.Repositories.Help=最初の行がプライマリ(最初に試行)、以降はフォールバックです。これらのリポジトリは環境ファイル内にあり(hop-config ではありません)。パスワードはコミットしないでください。 +HopEnvironmentDialog.Plugins.Help=インストールする Marketplace プラグイン。Group ID を空にすると org.apache.hop を使用します。適用時に全般タブの Hop バージョンが既定になることがあります。 +HopEnvironmentDialog.Dependencies.Help=オプションの Maven JAR(例: JDBC ドライバ)。Hop ホームのターゲットディレクトリ配下にインストール(既定 lib/jdbc)。 +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=ユーザー名 +HopEnvironmentDialog.Repo.Column.Password=パスワード +HopEnvironmentDialog.Repo.Column.Auth=認証 +HopEnvironmentDialog.Repo.Edit.Add=リポジトリを追加 +HopEnvironmentDialog.Repo.Edit.Edit=リポジトリを編集 +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=バージョン +HopEnvironmentDialog.Plugin.Edit.Add=プラグインを追加 +HopEnvironmentDialog.Plugin.Edit.Edit=プラグインを編集 +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=バージョン +HopEnvironmentDialog.Dep.Column.Target=ターゲット +HopEnvironmentDialog.Dep.Edit.Add=依存関係を追加 +HopEnvironmentDialog.Dep.Edit.Edit=依存関係を編集 +HopEnvironmentDialog.Dirty.Header=未保存の変更 +HopEnvironmentDialog.Dirty.Message=続行する前に環境ファイルの変更を保存しますか? +HopEnvironmentDialog.SaveFirst.Header=環境ファイルを保存しますか? +HopEnvironmentDialog.SaveFirst.Message=検証または適用の前に環境ファイルを保存しますか? +HopEnvironmentDialog.Validation.Header=無効な環境 +HopEnvironmentDialog.Validation.RepoIdUrl=各リポジトリには Id と URL が必要です。 +HopEnvironmentDialog.Validation.PluginArtifact=各プラグインには Artifact ID が必要です。 +HopEnvironmentDialog.Validation.DepGav=各依存関係には Group ID、Artifact ID、Version が必要です。 +HopEnvironmentDialog.ImportRepos.Header=リポジトリをインポート +HopEnvironmentDialog.ImportRepos.Message=リポジトリ一覧を hop-config.json の有効なリポジトリで置き換えますか? +HopEnvironmentDialog.Error.Header=環境エディタエラー +HopEnvironmentDialog.Error.Load=環境ファイルを読み込めませんでした:\n{0} +HopEnvironmentDialog.Error.Save=環境ファイルを保存できませんでした:\n{0} +HopEnvironmentDialog.Error.ImportRepos=hop-config からリポジトリをインポートできませんでした +AddPluginsFromCatalogDialog.Shell.Title=カタログからプラグインを追加 +AddPluginsFromCatalogDialog.Filter.Label=フィルタ +AddPluginsFromCatalogDialog.Button.Add=選択を追加 +AddPluginsFromCatalogDialog.Button.Cancel=キャンセル +AddPluginsFromCatalogDialog.Column.Name=名前 +AddPluginsFromCatalogDialog.Column.Artifact=アーティファクト +AddPluginsFromCatalogDialog.Column.Category=カテゴリ +AddPluginsFromCatalogDialog.Column.Description=説明 diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ko_KR.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ko_KR.properties new file mode 100644 index 00000000000..edd62e6877a --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_ko_KR.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace 플러그인 +MarketplaceLifecycleEnv.Help=플러그인 집합은 hop-env 파일에 정의됩니다. 이 옵션은 이 라이프사이클 환경에서 Hop이 해당 파일을 언제 검사할지 제어합니다(개발은 유연, 운영은 엄격). +MarketplaceLifecycleEnv.EnvFile.Label=환경 파일 +MarketplaceLifecycleEnv.Button.Browse=찾아보기… +MarketplaceLifecycleEnv.Button.EditEnv=편집… +MarketplaceLifecycleEnv.OnEnable.Label=활성화 시 +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: 검사 안 함; warn: 불일치 시 로그/대화상자; enforce: 불일치 시 환경 활성화 실패 +MarketplaceLifecycleEnv.Strict.Label=엄격(추가 Marketplace 플러그인이 설치되어 있으면 실패) +MarketplaceLifecycleEnv.Strict.Tooltip=검증 시 환경 파일에 없는 영수증이 있는 Marketplace 플러그인도 불일치로 처리합니다 +MarketplaceLifecycleEnv.AutoApply.Label=활성화 시 누락된 플러그인 자동 적용 +MarketplaceLifecycleEnv.AutoApply.Tooltip=활성화하면 누락된 플러그인에 대해 marketplace apply를 실행합니다(통제된 머신만; 기본 꺼짐) +MarketplaceDialog.Shell.Title=플러그인 Marketplace +MarketplaceDialog.Tab.Plugins=플러그인 +MarketplaceDialog.Tab.Environment=환경 +MarketplaceDialog.Tab.Repositories=리포지토리 +MarketplaceDialog.Repository.Label=기본 리포지토리 +MarketplaceDialog.Repository.SetPrimary.Error=기본 리포지토리를 설정할 수 없습니다 +MarketplaceDialog.Column.Name=이름 +MarketplaceDialog.Column.Artifact=아티팩트 +MarketplaceDialog.Column.Category=범주 +MarketplaceDialog.Column.Status=상태 +MarketplaceDialog.Column.Description=설명 +MarketplaceDialog.Search.Label=검색 +MarketplaceDialog.Search.Placeholder=플러그인 필터링을 위해 3자 이상 입력… +MarketplaceDialog.Search.Clear=검색 필터 지우기 +MarketplaceDialog.Button.Install=설치 +MarketplaceDialog.Button.Uninstall=제거 +MarketplaceDialog.Button.Refresh=새로고침 +MarketplaceDialog.Button.Close=닫기 +MarketplaceDialog.Button.ManageRepos=관리… +MarketplaceDialog.Button.BrowseEnv=찾아보기… +MarketplaceDialog.Button.EditEnv=편집… +MarketplaceDialog.Button.EditEnv.Tooltip=환경 파일 생성 또는 편집(리포지토리, 플러그인, 종속성) +MarketplaceDialog.Button.ValidateEnv=검증 +MarketplaceDialog.Button.ValidateEnv.Tooltip=설치하지 않고 로컬 설치를 환경 파일과 대조합니다 +MarketplaceDialog.Button.ApplyEnv=적용 +MarketplaceDialog.Button.ApplyEnv.Tooltip=환경 파일에 선언된 플러그인 및 종속성을 설치합니다 +MarketplaceDialog.EnvFile.Label=환경 파일 +MarketplaceDialog.EnvFile.Prune=적용 시 추가 항목 제거 +MarketplaceDialog.EnvFile.Prune.Tooltip=적용 시 파일에 없는 영수증이 있는 Marketplace 플러그인을 제거합니다 +MarketplaceDialog.EnvFile.Strict=엄격 검증 +MarketplaceDialog.EnvFile.Strict.Tooltip=검증 시 파일 범위를 넘는 추가 Marketplace 플러그인이 설치되어 있어도 실패합니다 +MarketplaceDialog.EnvFile.Filter.Env=Hop 환경 파일 (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=모든 파일 +MarketplaceDialog.EnvFile.Missing.Header=환경 파일 필요 +MarketplaceDialog.EnvFile.Missing.Message=찾아보기…로 hop-env.yaml(또는 full-client-env.yaml)을 선택하거나 경로를 입력하세요. +MarketplaceDialog.EnvFile.NotFound.Header=파일을 찾을 수 없음 +MarketplaceDialog.EnvFile.NotFound.Message=환경 파일이 존재하지 않습니다:\n{0} +MarketplaceDialog.Status.Installed=설치됨 +MarketplaceDialog.Status.Present=디스크에 있음 +MarketplaceDialog.Status.NotInstalled=설치되지 않음 +MarketplaceDialog.Status.RestartHint=설치·적용·제거 후 플러그인을 다시 로드하려면 Hop을 다시 시작하세요. 설치는 기본 리포지토리를 먼저 시도한 뒤 다른 사용 리포지토리를 사용합니다. +MarketplaceDialog.Status.Installing={0} 설치 중... +MarketplaceDialog.Status.Validating=환경 파일 검증 중... +MarketplaceDialog.Status.Applying=환경 파일 적용 중... +MarketplaceDialog.Status.Drift=환경 불일치 감지 — 적용으로 수정하세요 +MarketplaceDialog.Error.Header=Marketplace 오류 +MarketplaceDialog.Error.ResolveHome=Hop 설치 디렉터리를 확인할 수 없습니다. 설치 루트에서 hop 런처로 시작하세요. +MarketplaceDialog.Install.Done.Header=플러그인 설치됨 +MarketplaceDialog.Install.Done.Message={0}을(를) 설치했습니다.\n\n플러그인을 로드하려면 Hop을 다시 시작하세요. +MarketplaceDialog.Install.Error={0} 설치 실패 +MarketplaceDialog.Uninstall.Confirm.Header=플러그인 제거 +MarketplaceDialog.Uninstall.Confirm.Message=Marketplace로 설치한 플러그인 {0}을(를) 제거할까요? +MarketplaceDialog.Uninstall.NoReceipt.Header=제거할 수 없음 +MarketplaceDialog.Uninstall.NoReceipt.Message={0}에 대한 Marketplace 설치 영수증이 없습니다. 여기서는 Marketplace로 설치한 플러그인만 제거할 수 있습니다. +MarketplaceDialog.Uninstall.Error={0} 제거 실패 +MarketplaceDialog.Validate.Ok.Header=환경 정상 +MarketplaceDialog.Validate.Ok.Message=로컬 설치가 일치합니다:\n{0} +MarketplaceDialog.Validate.Drift.Header=환경 불일치 +MarketplaceDialog.Validate.Drift.Message=로컬 설치가 환경 파일과 일치하지 않습니다: +MarketplaceDialog.Validate.Drift.Hint=적용으로 누락된 플러그인을 설치하거나 버전을 수정한 뒤 Hop을 다시 시작하세요. +MarketplaceDialog.Validate.Error=환경 파일 검증 실패 +MarketplaceDialog.Apply.PruneConfirm.Header=Marketplace 플러그인을 정리할까요? +MarketplaceDialog.Apply.PruneConfirm.Message=정리와 함께 적용하면 환경 파일에 없는 Marketplace 플러그인이 제거됩니다.\n\n계속할까요? +MarketplaceDialog.Apply.Disabled=Marketplace가 hop-config.json에서 비활성화되어 있습니다. +MarketplaceDialog.Apply.Done.Header=환경 적용됨 +MarketplaceDialog.Apply.Done.Message=적용됨:\n{0}\n\n새 플러그인을 로드하려면 Hop을 다시 시작하세요. +MarketplaceDialog.Apply.Error=환경 파일 적용 실패 +ManageRepositoriesDialog.Shell.Title=Marketplace 리포지토리 +ManageRepositoriesDialog.Help=기본 리포지토리를 먼저 시도합니다. 다른 사용 리포지토리는 대체입니다.\n기본값: Apache Repository(repository.apache.org) 다음 Maven Central.\n설정은 '${HOP_CONFIG_FOLDER}'의 hop-config.json에 저장됩니다. +ManageRepositoriesDialog.Column.Primary=기본 +ManageRepositoriesDialog.Column.Enabled=사용 +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=이름 +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=인증 +ManageRepositoriesDialog.Button.Add=추가 +ManageRepositoriesDialog.Button.Edit=편집 +ManageRepositoriesDialog.Button.Remove=제거 +ManageRepositoriesDialog.Button.SetPrimary=기본으로 설정 +ManageRepositoriesDialog.Button.MoveUp=위로 +ManageRepositoriesDialog.Button.MoveDown=아래로 +ManageRepositoriesDialog.Button.ResetDefaults=기본값으로 재설정 +ManageRepositoriesDialog.Button.Save=저장 +ManageRepositoriesDialog.Button.Cancel=취소 +ManageRepositoriesDialog.Button.Ok=확인 +ManageRepositoriesDialog.Save.Done.Header=리포지토리 저장됨 +ManageRepositoriesDialog.Save.Done.Message=Marketplace 리포지토리 설정이 hop-config.json에 저장되었습니다. +ManageRepositoriesDialog.Error.Header=리포지토리 오류 +ManageRepositoriesDialog.Error.Save=Marketplace 구성을 저장할 수 없습니다 +ManageRepositoriesDialog.Reset.Header=리포지토리 재설정 +ManageRepositoriesDialog.Reset.Message=리포지토리 목록을 Apache Repository(기본)와 Maven Central로 바꿀까요? +ManageRepositoriesDialog.Edit.Title.Add=리포지토리 추가 +ManageRepositoriesDialog.Edit.Title.Edit=리포지토리 편집 +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=이름 +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=사용자 이름 +ManageRepositoriesDialog.Edit.Password=비밀번호 +ManageRepositoriesDialog.Edit.Enabled=사용(대체 체인에서 사용) +ManageRepositoriesDialog.Edit.Primary=기본(먼저 시도) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id와 URL이 필요합니다. +HopEnvironmentDialog.Shell.Title=Marketplace 환경 — {0} +HopEnvironmentDialog.Untitled=제목 없음 +HopEnvironmentDialog.Button.New=새로 만들기 +HopEnvironmentDialog.Button.Open=열기… +HopEnvironmentDialog.Button.Save=저장 +HopEnvironmentDialog.Button.SaveAs=다른 이름으로 저장… +HopEnvironmentDialog.Button.Validate=검증 +HopEnvironmentDialog.Button.Validate.Tooltip=설치하지 않고 로컬 설치를 이 환경 파일과 대조합니다 +HopEnvironmentDialog.Button.Apply=적용 +HopEnvironmentDialog.Button.Apply.Tooltip=이 환경 파일에 선언된 플러그인 및 종속성을 설치합니다 +HopEnvironmentDialog.Button.Close=닫기 +HopEnvironmentDialog.Button.Ok=확인 +HopEnvironmentDialog.Button.Cancel=취소 +HopEnvironmentDialog.Button.Add=추가 +HopEnvironmentDialog.Button.Edit=편집 +HopEnvironmentDialog.Button.Remove=제거 +HopEnvironmentDialog.Button.MoveUp=위로 +HopEnvironmentDialog.Button.MoveDown=아래로 +HopEnvironmentDialog.Button.AddFromCatalog=카탈로그에서… +HopEnvironmentDialog.Button.ImportRepos=hop-config에서 가져오기… +HopEnvironmentDialog.Prune=적용 시 추가 항목 제거 +HopEnvironmentDialog.Prune.Tooltip=적용 시 파일에 없는 영수증이 있는 Marketplace 플러그인을 제거합니다 +HopEnvironmentDialog.Strict=엄격 검증 +HopEnvironmentDialog.Strict.Tooltip=검증 시 파일 범위를 넘는 추가 Marketplace 플러그인이 설치되어 있어도 실패합니다 +HopEnvironmentDialog.Filter.Yaml=YAML 환경 (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON 환경 (*.json) +HopEnvironmentDialog.Tab.General=일반 +HopEnvironmentDialog.Tab.Repositories=리포지토리 +HopEnvironmentDialog.Tab.Plugins=플러그인 +HopEnvironmentDialog.Tab.Dependencies=종속성 +HopEnvironmentDialog.General.Help=이 Hop 설치에 있어야 할 플러그인과 jar를 선언하세요. CI/Docker용 hop-env.yaml로 저장하거나 이 대화상자에서 검증/적용을 사용하세요. +HopEnvironmentDialog.General.Version=스펙 버전 +HopEnvironmentDialog.General.HopVersion=Hop 버전 +HopEnvironmentDialog.General.EnforceOnRun=hop-run에서 강제 +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=true이면 설치가 이 파일에서 벗어나면 hop-run이 실패할 수 있습니다(-Dhop.env.enforce=true로도 제어) +HopEnvironmentDialog.Repositories.Help=첫 행이 기본(먼저 시도)이고 이후 행은 대체입니다. 이 리포지토리는 환경 파일에 있으며(hop-config 아님) 비밀번호는 커밋하지 않는 것이 좋습니다. +HopEnvironmentDialog.Plugins.Help=설치할 Marketplace 플러그인. Group ID를 비우면 org.apache.hop을 사용합니다. 적용 시 일반 탭의 Hop 버전이 기본값이 될 수 있습니다. +HopEnvironmentDialog.Dependencies.Help=선택적 Maven jar(예: JDBC 드라이버). Hop 홈의 대상 디렉터리 아래에 설치됩니다(기본 lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=사용자 이름 +HopEnvironmentDialog.Repo.Column.Password=비밀번호 +HopEnvironmentDialog.Repo.Column.Auth=인증 +HopEnvironmentDialog.Repo.Edit.Add=리포지토리 추가 +HopEnvironmentDialog.Repo.Edit.Edit=리포지토리 편집 +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=버전 +HopEnvironmentDialog.Plugin.Edit.Add=플러그인 추가 +HopEnvironmentDialog.Plugin.Edit.Edit=플러그인 편집 +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=버전 +HopEnvironmentDialog.Dep.Column.Target=대상 +HopEnvironmentDialog.Dep.Edit.Add=종속성 추가 +HopEnvironmentDialog.Dep.Edit.Edit=종속성 편집 +HopEnvironmentDialog.Dirty.Header=저장되지 않은 변경 사항 +HopEnvironmentDialog.Dirty.Message=계속하기 전에 환경 파일 변경 사항을 저장할까요? +HopEnvironmentDialog.SaveFirst.Header=환경 파일을 저장할까요? +HopEnvironmentDialog.SaveFirst.Message=검증 또는 적용 전에 환경 파일을 저장할까요? +HopEnvironmentDialog.Validation.Header=잘못된 환경 +HopEnvironmentDialog.Validation.RepoIdUrl=각 리포지토리에는 Id와 URL이 필요합니다. +HopEnvironmentDialog.Validation.PluginArtifact=각 플러그인에는 Artifact ID가 필요합니다. +HopEnvironmentDialog.Validation.DepGav=각 종속성에는 Group ID, Artifact ID 및 Version이 필요합니다. +HopEnvironmentDialog.ImportRepos.Header=리포지토리 가져오기 +HopEnvironmentDialog.ImportRepos.Message=리포지토리 목록을 hop-config.json의 사용 중인 리포지토리로 바꿀까요? +HopEnvironmentDialog.Error.Header=환경 편집기 오류 +HopEnvironmentDialog.Error.Load=환경 파일을 로드할 수 없습니다:\n{0} +HopEnvironmentDialog.Error.Save=환경 파일을 저장할 수 없습니다:\n{0} +HopEnvironmentDialog.Error.ImportRepos=hop-config에서 리포지토리를 가져올 수 없습니다 +AddPluginsFromCatalogDialog.Shell.Title=카탈로그에서 플러그인 추가 +AddPluginsFromCatalogDialog.Filter.Label=필터 +AddPluginsFromCatalogDialog.Button.Add=선택 항목 추가 +AddPluginsFromCatalogDialog.Button.Cancel=취소 +AddPluginsFromCatalogDialog.Column.Name=이름 +AddPluginsFromCatalogDialog.Column.Artifact=아티팩트 +AddPluginsFromCatalogDialog.Column.Category=범주 +AddPluginsFromCatalogDialog.Column.Description=설명 diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_nl_NL.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_nl_NL.properties new file mode 100644 index 00000000000..1950e506b3c --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_nl_NL.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace-plugins +MarketplaceLifecycleEnv.Help=De pluginset wordt gedefinieerd in een hop-env-bestand. Deze opties bepalen wanneer Hop dat bestand controleert voor deze lifecycle-omgeving (los in development; strikt in productie). +MarketplaceLifecycleEnv.EnvFile.Label=Omgevingsbestand +MarketplaceLifecycleEnv.Button.Browse=Bladeren… +MarketplaceLifecycleEnv.Button.EditEnv=Bewerken… +MarketplaceLifecycleEnv.OnEnable.Label=Bij activeren +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: geen controle; warn: log/dialoog bij afwijking; enforce: activeren van de omgeving mislukt bij afwijking +MarketplaceLifecycleEnv.Strict.Label=Strikt (mislukken als er extra marketplace-plugins geïnstalleerd zijn) +MarketplaceLifecycleEnv.Strict.Tooltip=Bij validatie ook marketplace-plugins met receipt die niet in het env-bestand staan als afwijking behandelen +MarketplaceLifecycleEnv.AutoApply.Label=Ontbrekende plugins automatisch toepassen bij activeren +MarketplaceLifecycleEnv.AutoApply.Tooltip=Indien ingeschakeld, voert marketplace apply uit voor ontbrekende plugins (alleen op gecontroleerde machines; standaard uit) +MarketplaceDialog.Shell.Title=Plugin-marketplace +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Omgeving +MarketplaceDialog.Tab.Repositories=Repositories +MarketplaceDialog.Repository.Label=Primaire repository +MarketplaceDialog.Repository.SetPrimary.Error=Kon de primaire repository niet instellen +MarketplaceDialog.Column.Name=Naam +MarketplaceDialog.Column.Artifact=Artifact +MarketplaceDialog.Column.Category=Categorie +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Beschrijving +MarketplaceDialog.Search.Label=Zoeken +MarketplaceDialog.Search.Placeholder=Typ 3+ tekens om plugins te filteren… +MarketplaceDialog.Search.Clear=Zoekfilter wissen +MarketplaceDialog.Button.Install=Installeren +MarketplaceDialog.Button.Uninstall=Deïnstalleren +MarketplaceDialog.Button.Refresh=Vernieuwen +MarketplaceDialog.Button.Close=Sluiten +MarketplaceDialog.Button.ManageRepos=Beheren… +MarketplaceDialog.Button.BrowseEnv=Bladeren… +MarketplaceDialog.Button.EditEnv=Bewerken… +MarketplaceDialog.Button.EditEnv.Tooltip=Omgevingsbestand maken of bewerken (repositories, plugins, afhankelijkheden) +MarketplaceDialog.Button.ValidateEnv=Valideren +MarketplaceDialog.Button.ValidateEnv.Tooltip=Lokale installatie controleren tegen het omgevingsbestand zonder te installeren +MarketplaceDialog.Button.ApplyEnv=Toepassen +MarketplaceDialog.Button.ApplyEnv.Tooltip=Plugins en afhankelijkheden uit het omgevingsbestand installeren +MarketplaceDialog.EnvFile.Label=Omgevingsbestand +MarketplaceDialog.EnvFile.Prune=Extra's bij toepassen opruimen +MarketplaceDialog.EnvFile.Prune.Tooltip=Bij toepassen marketplace-plugins met receipt deïnstalleren die niet in het bestand staan +MarketplaceDialog.EnvFile.Strict=Strikte validatie +MarketplaceDialog.EnvFile.Strict.Tooltip=Bij validatie ook mislukken als er extra marketplace-plugins buiten het bestand geïnstalleerd zijn +MarketplaceDialog.EnvFile.Filter.Env=Hop-omgevingsbestanden (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Alle bestanden +MarketplaceDialog.EnvFile.Missing.Header=Omgevingsbestand vereist +MarketplaceDialog.EnvFile.Missing.Message=Selecteer een hop-env.yaml (of full-client-env.yaml) met Bladeren…, of typ het pad. +MarketplaceDialog.EnvFile.NotFound.Header=Bestand niet gevonden +MarketplaceDialog.EnvFile.NotFound.Message=Omgevingsbestand bestaat niet:\n{0} +MarketplaceDialog.Status.Installed=Geïnstalleerd +MarketplaceDialog.Status.Present=Aanwezig op schijf +MarketplaceDialog.Status.NotInstalled=Niet geïnstalleerd +MarketplaceDialog.Status.RestartHint=Herstart Hop na installeren, toepassen of deïnstalleren zodat plugins opnieuw worden geladen. Installatie probeert eerst de primaire repository, daarna andere ingeschakelde repositories. +MarketplaceDialog.Status.Installing={0} installeren... +MarketplaceDialog.Status.Validating=Omgevingsbestand valideren... +MarketplaceDialog.Status.Applying=Omgevingsbestand toepassen... +MarketplaceDialog.Status.Drift=Omgevingsafwijking gedetecteerd — gebruik Toepassen om te herstellen +MarketplaceDialog.Error.Header=Marketplace-fout +MarketplaceDialog.Error.ResolveHome=Kon de Hop-installatiemap niet bepalen. Start Hop vanaf de installatieroot met de hop-launcher. +MarketplaceDialog.Install.Done.Header=Plugin geïnstalleerd +MarketplaceDialog.Install.Done.Message={0} geïnstalleerd.\n\nHerstart Hop om de plugin te laden. +MarketplaceDialog.Install.Error=Installatie van {0} mislukt +MarketplaceDialog.Uninstall.Confirm.Header=Plugin deïnstalleren +MarketplaceDialog.Uninstall.Confirm.Message=Via marketplace geïnstalleerde plugin {0} verwijderen? +MarketplaceDialog.Uninstall.NoReceipt.Header=Kan niet deïnstalleren +MarketplaceDialog.Uninstall.NoReceipt.Message=Geen marketplace-installatiebewijs voor {0}. Hier kunnen alleen via de marketplace geïnstalleerde plugins worden deïnstalleerd. +MarketplaceDialog.Uninstall.Error=Deïnstallatie van {0} mislukt +MarketplaceDialog.Validate.Ok.Header=Omgeving OK +MarketplaceDialog.Validate.Ok.Message=Lokale installatie komt overeen:\n{0} +MarketplaceDialog.Validate.Drift.Header=Omgevingsafwijking +MarketplaceDialog.Validate.Drift.Message=De lokale installatie komt niet overeen met het omgevingsbestand: +MarketplaceDialog.Validate.Drift.Hint=Gebruik Toepassen om ontbrekende plugins te installeren (of versies te herstellen) en herstart Hop. +MarketplaceDialog.Validate.Error=Validatie van omgevingsbestand mislukt +MarketplaceDialog.Apply.PruneConfirm.Header=Marketplace-plugins opruimen? +MarketplaceDialog.Apply.PruneConfirm.Message=Toepassen met opruimen deïnstalleert marketplace-plugins die niet in het omgevingsbestand staan.\n\nDoorgaan? +MarketplaceDialog.Apply.Disabled=Marketplace is uitgeschakeld in hop-config.json. +MarketplaceDialog.Apply.Done.Header=Omgeving toegepast +MarketplaceDialog.Apply.Done.Message=Toegepast:\n{0}\n\nHerstart Hop om nieuwe plugins te laden. +MarketplaceDialog.Apply.Error=Toepassen van omgevingsbestand mislukt +ManageRepositoriesDialog.Shell.Title=Marketplace-repositories +ManageRepositoriesDialog.Help=De primaire repository wordt eerst geprobeerd. De overige ingeschakelde repositories zijn fallbacks.\nStandaard: Apache Repository (repository.apache.org) en daarna Maven Central.\nInstellingen worden opgeslagen in hop-config.json onder '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Primair +ManageRepositoriesDialog.Column.Enabled=Ingeschakeld +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Naam +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Toevoegen +ManageRepositoriesDialog.Button.Edit=Bewerken +ManageRepositoriesDialog.Button.Remove=Verwijderen +ManageRepositoriesDialog.Button.SetPrimary=Als primair instellen +ManageRepositoriesDialog.Button.MoveUp=Omhoog +ManageRepositoriesDialog.Button.MoveDown=Omlaag +ManageRepositoriesDialog.Button.ResetDefaults=Standaardwaarden herstellen +ManageRepositoriesDialog.Button.Save=Opslaan +ManageRepositoriesDialog.Button.Cancel=Annuleren +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repositories opgeslagen +ManageRepositoriesDialog.Save.Done.Message=Marketplace-repository-instellingen zijn opgeslagen in hop-config.json. +ManageRepositoriesDialog.Error.Header=Repositoryfout +ManageRepositoriesDialog.Error.Save=Kon marketplace-configuratie niet opslaan +ManageRepositoriesDialog.Reset.Header=Repositories herstellen +ManageRepositoriesDialog.Reset.Message=Repositorylijst vervangen door Apache Repository (primair) en Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Repository toevoegen +ManageRepositoriesDialog.Edit.Title.Edit=Repository bewerken +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Naam +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Gebruikersnaam +ManageRepositoriesDialog.Edit.Password=Wachtwoord +ManageRepositoriesDialog.Edit.Enabled=Ingeschakeld (gebruiken in fallback-keten) +ManageRepositoriesDialog.Edit.Primary=Primair (eerst geprobeerd) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id en URL zijn verplicht. +HopEnvironmentDialog.Shell.Title=Marketplace-omgeving — {0} +HopEnvironmentDialog.Untitled=Naamloos +HopEnvironmentDialog.Button.New=Nieuw +HopEnvironmentDialog.Button.Open=Openen… +HopEnvironmentDialog.Button.Save=Opslaan +HopEnvironmentDialog.Button.SaveAs=Opslaan als… +HopEnvironmentDialog.Button.Validate=Valideren +HopEnvironmentDialog.Button.Validate.Tooltip=Lokale installatie controleren tegen dit omgevingsbestand zonder te installeren +HopEnvironmentDialog.Button.Apply=Toepassen +HopEnvironmentDialog.Button.Apply.Tooltip=Plugins en afhankelijkheden uit dit omgevingsbestand installeren +HopEnvironmentDialog.Button.Close=Sluiten +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Annuleren +HopEnvironmentDialog.Button.Add=Toevoegen +HopEnvironmentDialog.Button.Edit=Bewerken +HopEnvironmentDialog.Button.Remove=Verwijderen +HopEnvironmentDialog.Button.MoveUp=Omhoog +HopEnvironmentDialog.Button.MoveDown=Omlaag +HopEnvironmentDialog.Button.AddFromCatalog=Uit catalogus… +HopEnvironmentDialog.Button.ImportRepos=Importeren uit hop-config… +HopEnvironmentDialog.Prune=Extra's bij toepassen opruimen +HopEnvironmentDialog.Prune.Tooltip=Bij toepassen marketplace-plugins met receipt deïnstalleren die niet in het bestand staan +HopEnvironmentDialog.Strict=Strikte validatie +HopEnvironmentDialog.Strict.Tooltip=Bij validatie ook mislukken als er extra marketplace-plugins buiten het bestand geïnstalleerd zijn +HopEnvironmentDialog.Filter.Yaml=YAML-omgeving (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON-omgeving (*.json) +HopEnvironmentDialog.Tab.General=Algemeen +HopEnvironmentDialog.Tab.Repositories=Repositories +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Afhankelijkheden +HopEnvironmentDialog.General.Help=Geef aan welke plugins en jars deze Hop-installatie moet hebben. Sla op als hop-env.yaml voor CI/Docker, of gebruik Valideren / Toepassen in dit dialoogvenster. +HopEnvironmentDialog.General.Version=Specificatieversie +HopEnvironmentDialog.General.HopVersion=Hop-versie +HopEnvironmentDialog.General.EnforceOnRun=Afdwingen bij hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Indien true kan hop-run mislukken als de installatie van dit bestand afwijkt (ook gestuurd door -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=De eerste rij is primair (eerst geprobeerd); latere rijen zijn fallbacks. Deze repositories staan in het omgevingsbestand (niet hop-config). Commit bij voorkeur geen wachtwoorden. +HopEnvironmentDialog.Plugins.Help=Te installeren marketplace-plugins. Laat Group ID leeg voor org.apache.hop. Versie kan bij toepassen van de Hop-versie op het tabblad Algemeen komen. +HopEnvironmentDialog.Dependencies.Help=Optionele Maven-jars (bijv. JDBC-drivers) geïnstalleerd onder de doelmap van Hop home (standaard lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Gebruikersnaam +HopEnvironmentDialog.Repo.Column.Password=Wachtwoord +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Repository toevoegen +HopEnvironmentDialog.Repo.Edit.Edit=Repository bewerken +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versie +HopEnvironmentDialog.Plugin.Edit.Add=Plugin toevoegen +HopEnvironmentDialog.Plugin.Edit.Edit=Plugin bewerken +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versie +HopEnvironmentDialog.Dep.Column.Target=Doel +HopEnvironmentDialog.Dep.Edit.Add=Afhankelijkheid toevoegen +HopEnvironmentDialog.Dep.Edit.Edit=Afhankelijkheid bewerken +HopEnvironmentDialog.Dirty.Header=Niet-opgeslagen wijzigingen +HopEnvironmentDialog.Dirty.Message=Wijzigingen in het omgevingsbestand opslaan voordat u doorgaat? +HopEnvironmentDialog.SaveFirst.Header=Omgevingsbestand opslaan? +HopEnvironmentDialog.SaveFirst.Message=Omgevingsbestand opslaan vóór Valideren of Toepassen? +HopEnvironmentDialog.Validation.Header=Ongeldige omgeving +HopEnvironmentDialog.Validation.RepoIdUrl=Elke repository heeft een Id en URL nodig. +HopEnvironmentDialog.Validation.PluginArtifact=Elke plugin heeft een Artifact ID nodig. +HopEnvironmentDialog.Validation.DepGav=Elke afhankelijkheid heeft Group ID, Artifact ID en Version nodig. +HopEnvironmentDialog.ImportRepos.Header=Repositories importeren +HopEnvironmentDialog.ImportRepos.Message=Repositorylijst vervangen door ingeschakelde repositories uit hop-config.json? +HopEnvironmentDialog.Error.Header=Fout in omgevingseditor +HopEnvironmentDialog.Error.Load=Kon omgevingsbestand niet laden:\n{0} +HopEnvironmentDialog.Error.Save=Kon omgevingsbestand niet opslaan:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Kon repositories niet importeren uit hop-config +AddPluginsFromCatalogDialog.Shell.Title=Plugins uit catalogus toevoegen +AddPluginsFromCatalogDialog.Filter.Label=Filter +AddPluginsFromCatalogDialog.Button.Add=Selectie toevoegen +AddPluginsFromCatalogDialog.Button.Cancel=Annuleren +AddPluginsFromCatalogDialog.Column.Name=Naam +AddPluginsFromCatalogDialog.Column.Artifact=Artifact +AddPluginsFromCatalogDialog.Column.Category=Categorie +AddPluginsFromCatalogDialog.Column.Description=Beschrijving diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_no_NO.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_no_NO.properties new file mode 100644 index 00000000000..5d95bccb61c --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_no_NO.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace-plugins +MarketplaceLifecycleEnv.Help=Plugin-settet defineres i en hop-env-fil. Disse alternativene styrer når Hop sjekker filen for dette livssyklusmiljøet (løst i dev; strengt i produksjon). +MarketplaceLifecycleEnv.EnvFile.Label=Miljøfil +MarketplaceLifecycleEnv.Button.Browse=Bla gjennom… +MarketplaceLifecycleEnv.Button.EditEnv=Rediger… +MarketplaceLifecycleEnv.OnEnable.Label=Ved aktivering +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: ingen sjekk; warn: logg/dialog ved avvik; enforce: mislykk aktivering ved avvik +MarketplaceLifecycleEnv.Strict.Label=Streng (mislykkes hvis ekstra marketplace-plugins er installert) +MarketplaceLifecycleEnv.Strict.Tooltip=Ved validering, behandle også marketplace-plugins med kvittering som ikke er i env-filen som avvik +MarketplaceLifecycleEnv.AutoApply.Label=Auto-bruk manglende plugins ved aktivering +MarketplaceLifecycleEnv.AutoApply.Tooltip=Når aktivert, kjører marketplace apply for manglende plugins (kun på kontrollerte maskiner; av som standard) +MarketplaceDialog.Shell.Title=Plugin-marketplace +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Miljø +MarketplaceDialog.Tab.Repositories=Depot +MarketplaceDialog.Repository.Label=Primært depot +MarketplaceDialog.Repository.SetPrimary.Error=Kunne ikke sette primært depot +MarketplaceDialog.Column.Name=Navn +MarketplaceDialog.Column.Artifact=Artifact +MarketplaceDialog.Column.Category=Kategori +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Beskrivelse +MarketplaceDialog.Search.Label=Søk +MarketplaceDialog.Search.Placeholder=Skriv 3+ tegn for å filtrere plugins… +MarketplaceDialog.Search.Clear=Tøm søkefilter +MarketplaceDialog.Button.Install=Installer +MarketplaceDialog.Button.Uninstall=Avinstaller +MarketplaceDialog.Button.Refresh=Oppdater +MarketplaceDialog.Button.Close=Lukk +MarketplaceDialog.Button.ManageRepos=Administrer… +MarketplaceDialog.Button.BrowseEnv=Bla gjennom… +MarketplaceDialog.Button.EditEnv=Rediger… +MarketplaceDialog.Button.EditEnv.Tooltip=Opprett eller rediger miljøfilen (depot, plugins, avhengigheter) +MarketplaceDialog.Button.ValidateEnv=Valider +MarketplaceDialog.Button.ValidateEnv.Tooltip=Sjekk den lokale installasjonen mot miljøfilen uten å installere +MarketplaceDialog.Button.ApplyEnv=Bruk +MarketplaceDialog.Button.ApplyEnv.Tooltip=Installer plugins og avhengigheter erklært i miljøfilen +MarketplaceDialog.EnvFile.Label=Miljøfil +MarketplaceDialog.EnvFile.Prune=Fjern ekstra ved bruk +MarketplaceDialog.EnvFile.Prune.Tooltip=Ved bruk, avinstaller marketplace-plugins med kvittering som ikke er i filen +MarketplaceDialog.EnvFile.Strict=Streng validering +MarketplaceDialog.EnvFile.Strict.Tooltip=Ved validering, mislykkes også hvis ekstra marketplace-plugins er installert utover filen +MarketplaceDialog.EnvFile.Filter.Env=Hop-miljøfiler (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Alle filer +MarketplaceDialog.EnvFile.Missing.Header=Miljøfil kreves +MarketplaceDialog.EnvFile.Missing.Message=Velg en hop-env.yaml (eller full-client-env.yaml) med Bla gjennom…, eller skriv inn banen. +MarketplaceDialog.EnvFile.NotFound.Header=Fil ikke funnet +MarketplaceDialog.EnvFile.NotFound.Message=Miljøfilen finnes ikke:\n{0} +MarketplaceDialog.Status.Installed=Installert +MarketplaceDialog.Status.Present=Tilstede på disk +MarketplaceDialog.Status.NotInstalled=Ikke installert +MarketplaceDialog.Status.RestartHint=Start Hop på nytt etter installasjon, bruk eller avinstallasjon slik at plugins lastes på nytt. Installasjon prøver primært depot først, deretter andre aktiverte depot. +MarketplaceDialog.Status.Installing=Installerer {0}... +MarketplaceDialog.Status.Validating=Validerer miljøfil... +MarketplaceDialog.Status.Applying=Bruker miljøfil... +MarketplaceDialog.Status.Drift=Miljøavvik oppdaget — bruk Bruk for å rette +MarketplaceDialog.Error.Header=Marketplace-feil +MarketplaceDialog.Error.ResolveHome=Kunne ikke fastslå Hop-installasjonskatalogen. Start Hop fra installasjonsroten med hop-starteren. +MarketplaceDialog.Install.Done.Header=Plugin installert +MarketplaceDialog.Install.Done.Message=Installert {0}.\n\nStart Hop på nytt for å laste pluginen. +MarketplaceDialog.Install.Error=Kunne ikke installere {0} +MarketplaceDialog.Uninstall.Confirm.Header=Avinstaller plugin +MarketplaceDialog.Uninstall.Confirm.Message=Fjerne marketplace-installert plugin {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Kan ikke avinstallere +MarketplaceDialog.Uninstall.NoReceipt.Message=Ingen marketplace-installasjonskvittering for {0}. Bare plugins installert via marketplace kan avinstalleres her. +MarketplaceDialog.Uninstall.Error=Kunne ikke avinstallere {0} +MarketplaceDialog.Validate.Ok.Header=Miljø OK +MarketplaceDialog.Validate.Ok.Message=Lokal installasjon samsvarer:\n{0} +MarketplaceDialog.Validate.Drift.Header=Miljøavvik +MarketplaceDialog.Validate.Drift.Message=Den lokale installasjonen samsvarer ikke med miljøfilen: +MarketplaceDialog.Validate.Drift.Hint=Bruk Bruk for å installere manglende plugins (eller rette versjoner), og start Hop på nytt. +MarketplaceDialog.Validate.Error=Kunne ikke validere miljøfilen +MarketplaceDialog.Apply.PruneConfirm.Header=Rydde marketplace-plugins? +MarketplaceDialog.Apply.PruneConfirm.Message=Bruk med opprydding avinstallerer marketplace-plugins som ikke er i miljøfilen.\n\nFortsette? +MarketplaceDialog.Apply.Disabled=Marketplace er deaktivert i hop-config.json. +MarketplaceDialog.Apply.Done.Header=Miljø brukt +MarketplaceDialog.Apply.Done.Message=Brukt:\n{0}\n\nStart Hop på nytt for å laste nye plugins. +MarketplaceDialog.Apply.Error=Kunne ikke bruke miljøfilen +ManageRepositoriesDialog.Shell.Title=Marketplace-depot +ManageRepositoriesDialog.Help=Det primære depotet prøves først. Andre aktiverte depot er reserve.\nStandard: Apache Repository (repository.apache.org) deretter Maven Central.\nInnstillinger lagres i hop-config.json under '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Primær +ManageRepositoriesDialog.Column.Enabled=Aktivert +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Navn +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Legg til +ManageRepositoriesDialog.Button.Edit=Rediger +ManageRepositoriesDialog.Button.Remove=Fjern +ManageRepositoriesDialog.Button.SetPrimary=Sett som primær +ManageRepositoriesDialog.Button.MoveUp=Opp +ManageRepositoriesDialog.Button.MoveDown=Ned +ManageRepositoriesDialog.Button.ResetDefaults=Tilbakestill standarder +ManageRepositoriesDialog.Button.Save=Lagre +ManageRepositoriesDialog.Button.Cancel=Avbryt +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Depot lagret +ManageRepositoriesDialog.Save.Done.Message=Marketplace-depotinnstillinger ble lagret i hop-config.json. +ManageRepositoriesDialog.Error.Header=Depotfeil +ManageRepositoriesDialog.Error.Save=Kunne ikke lagre marketplace-konfigurasjonen +ManageRepositoriesDialog.Reset.Header=Tilbakestill depot +ManageRepositoriesDialog.Reset.Message=Erstatte depotlisten med Apache Repository (primær) og Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Legg til depot +ManageRepositoriesDialog.Edit.Title.Edit=Rediger depot +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Navn +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Brukernavn +ManageRepositoriesDialog.Edit.Password=Passord +ManageRepositoriesDialog.Edit.Enabled=Aktivert (bruk i reservekjeden) +ManageRepositoriesDialog.Edit.Primary=Primær (prøves først) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id og URL er påkrevd. +HopEnvironmentDialog.Shell.Title=Marketplace-miljø — {0} +HopEnvironmentDialog.Untitled=Uten navn +HopEnvironmentDialog.Button.New=Ny +HopEnvironmentDialog.Button.Open=Åpne… +HopEnvironmentDialog.Button.Save=Lagre +HopEnvironmentDialog.Button.SaveAs=Lagre som… +HopEnvironmentDialog.Button.Validate=Valider +HopEnvironmentDialog.Button.Validate.Tooltip=Sjekk den lokale installasjonen mot denne miljøfilen uten å installere +HopEnvironmentDialog.Button.Apply=Bruk +HopEnvironmentDialog.Button.Apply.Tooltip=Installer plugins og avhengigheter erklært i denne miljøfilen +HopEnvironmentDialog.Button.Close=Lukk +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Avbryt +HopEnvironmentDialog.Button.Add=Legg til +HopEnvironmentDialog.Button.Edit=Rediger +HopEnvironmentDialog.Button.Remove=Fjern +HopEnvironmentDialog.Button.MoveUp=Opp +HopEnvironmentDialog.Button.MoveDown=Ned +HopEnvironmentDialog.Button.AddFromCatalog=Fra katalog… +HopEnvironmentDialog.Button.ImportRepos=Importer fra hop-config… +HopEnvironmentDialog.Prune=Fjern ekstra ved bruk +HopEnvironmentDialog.Prune.Tooltip=Ved bruk, avinstaller marketplace-plugins med kvittering som ikke er i filen +HopEnvironmentDialog.Strict=Streng validering +HopEnvironmentDialog.Strict.Tooltip=Ved validering, mislykkes også hvis ekstra marketplace-plugins er installert utover filen +HopEnvironmentDialog.Filter.Yaml=YAML-miljø (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON-miljø (*.json) +HopEnvironmentDialog.Tab.General=Generelt +HopEnvironmentDialog.Tab.Repositories=Depot +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Avhengigheter +HopEnvironmentDialog.General.Help=Angi hvilke plugins og jars denne Hop-installasjonen skal ha. Lagre som hop-env.yaml for CI/Docker, eller bruk Valider / Bruk i denne dialogen. +HopEnvironmentDialog.General.Version=Spesifikasjonsversjon +HopEnvironmentDialog.General.HopVersion=Hop-versjon +HopEnvironmentDialog.General.EnforceOnRun=Håndhev på hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Hvis true, kan hop-run mislykkes hvis installasjonen avviker fra denne filen (også styrt av -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=Første rad er primær (prøves først); senere rader er reserve. Disse depotene ligger i miljøfilen (ikke hop-config). Unngå å committe passord. +HopEnvironmentDialog.Plugins.Help=Marketplace-plugins som skal installeres. La Group ID stå tom for org.apache.hop. Versjon kan standard fra Hop-versjon på Generelt-fanen ved bruk. +HopEnvironmentDialog.Dependencies.Help=Valgfrie Maven-jars (f.eks. JDBC-drivere) installert under målkatalogen i Hop home (standard lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Brukernavn +HopEnvironmentDialog.Repo.Column.Password=Passord +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Legg til depot +HopEnvironmentDialog.Repo.Edit.Edit=Rediger depot +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versjon +HopEnvironmentDialog.Plugin.Edit.Add=Legg til plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Rediger plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versjon +HopEnvironmentDialog.Dep.Column.Target=Mål +HopEnvironmentDialog.Dep.Edit.Add=Legg til avhengighet +HopEnvironmentDialog.Dep.Edit.Edit=Rediger avhengighet +HopEnvironmentDialog.Dirty.Header=Ulagrede endringer +HopEnvironmentDialog.Dirty.Message=Lagre endringer i miljøfilen før du fortsetter? +HopEnvironmentDialog.SaveFirst.Header=Lagre miljøfil? +HopEnvironmentDialog.SaveFirst.Message=Lagre miljøfilen før Valider eller Bruk? +HopEnvironmentDialog.Validation.Header=Ugyldig miljø +HopEnvironmentDialog.Validation.RepoIdUrl=Hvert depot trenger en Id og URL. +HopEnvironmentDialog.Validation.PluginArtifact=Hver plugin trenger en Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Hver avhengighet trenger Group ID, Artifact ID og Version. +HopEnvironmentDialog.ImportRepos.Header=Importer depot +HopEnvironmentDialog.ImportRepos.Message=Erstatte depotlisten med aktiverte depot fra hop-config.json? +HopEnvironmentDialog.Error.Header=Feil i miljøredigering +HopEnvironmentDialog.Error.Load=Kunne ikke laste miljøfil:\n{0} +HopEnvironmentDialog.Error.Save=Kunne ikke lagre miljøfil:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Kunne ikke importere depot fra hop-config +AddPluginsFromCatalogDialog.Shell.Title=Legg til plugins fra katalog +AddPluginsFromCatalogDialog.Filter.Label=Filter +AddPluginsFromCatalogDialog.Button.Add=Legg til valgte +AddPluginsFromCatalogDialog.Button.Cancel=Avbryt +AddPluginsFromCatalogDialog.Column.Name=Navn +AddPluginsFromCatalogDialog.Column.Artifact=Artifact +AddPluginsFromCatalogDialog.Column.Category=Kategori +AddPluginsFromCatalogDialog.Column.Description=Beskrivelse diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pl_PL.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pl_PL.properties new file mode 100644 index 00000000000..b382add0da4 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pl_PL.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Wtyczki Marketplace +MarketplaceLifecycleEnv.Help=Zestaw wtyczek jest zdefiniowany w pliku hop-env. Te opcje kontrolują, kiedy Hop sprawdza ten plik dla tego środowiska cyklu życia (luźno w dev; rygorystycznie w produkcji). +MarketplaceLifecycleEnv.EnvFile.Label=Plik środowiska +MarketplaceLifecycleEnv.Button.Browse=Przeglądaj… +MarketplaceLifecycleEnv.Button.EditEnv=Edytuj… +MarketplaceLifecycleEnv.OnEnable.Label=Przy włączaniu +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: bez sprawdzenia; warn: log/dialog przy rozbieżności; enforce: niepowodzenie włączania środowiska przy rozbieżności +MarketplaceLifecycleEnv.Strict.Label=Ścisłe (niepowodzenie, jeśli zainstalowano dodatkowe wtyczki marketplace) +MarketplaceLifecycleEnv.Strict.Tooltip=Przy walidacji traktuj też wtyczki marketplace z potwierdzeniem spoza pliku env jako rozbieżność +MarketplaceLifecycleEnv.AutoApply.Label=Automatycznie zastosuj brakujące wtyczki przy włączaniu +MarketplaceLifecycleEnv.AutoApply.Tooltip=Gdy włączone, uruchamia marketplace apply dla brakujących wtyczek (tylko na kontrolowanych maszynach; domyślnie wyłączone) +MarketplaceDialog.Shell.Title=Marketplace wtyczek +MarketplaceDialog.Tab.Plugins=Wtyczki +MarketplaceDialog.Tab.Environment=Środowisko +MarketplaceDialog.Tab.Repositories=Repozytoria +MarketplaceDialog.Repository.Label=Główne repozytorium +MarketplaceDialog.Repository.SetPrimary.Error=Nie można ustawić głównego repozytorium +MarketplaceDialog.Column.Name=Nazwa +MarketplaceDialog.Column.Artifact=Artefakt +MarketplaceDialog.Column.Category=Kategoria +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Opis +MarketplaceDialog.Search.Label=Szukaj +MarketplaceDialog.Search.Placeholder=Wpisz 3+ znaki, aby filtrować wtyczki… +MarketplaceDialog.Search.Clear=Wyczyść filtr wyszukiwania +MarketplaceDialog.Button.Install=Zainstaluj +MarketplaceDialog.Button.Uninstall=Odinstaluj +MarketplaceDialog.Button.Refresh=Odśwież +MarketplaceDialog.Button.Close=Zamknij +MarketplaceDialog.Button.ManageRepos=Zarządzaj… +MarketplaceDialog.Button.BrowseEnv=Przeglądaj… +MarketplaceDialog.Button.EditEnv=Edytuj… +MarketplaceDialog.Button.EditEnv.Tooltip=Utwórz lub edytuj plik środowiska (repozytoria, wtyczki, zależności) +MarketplaceDialog.Button.ValidateEnv=Waliduj +MarketplaceDialog.Button.ValidateEnv.Tooltip=Sprawdź lokalną instalację względem pliku środowiska bez instalowania +MarketplaceDialog.Button.ApplyEnv=Zastosuj +MarketplaceDialog.Button.ApplyEnv.Tooltip=Zainstaluj wtyczki i zależności zadeklarowane w pliku środowiska +MarketplaceDialog.EnvFile.Label=Plik środowiska +MarketplaceDialog.EnvFile.Prune=Usuń dodatkowe przy zastosowaniu +MarketplaceDialog.EnvFile.Prune.Tooltip=Przy zastosowaniu odinstaluj wtyczki marketplace z potwierdzeniem spoza pliku +MarketplaceDialog.EnvFile.Strict=Ścisła walidacja +MarketplaceDialog.EnvFile.Strict.Tooltip=Przy walidacji niepowodzenie także, gdy zainstalowano dodatkowe wtyczki marketplace poza plikiem +MarketplaceDialog.EnvFile.Filter.Env=Pliki środowiska Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Wszystkie pliki +MarketplaceDialog.EnvFile.Missing.Header=Wymagany plik środowiska +MarketplaceDialog.EnvFile.Missing.Message=Wybierz hop-env.yaml (lub full-client-env.yaml) przez Przeglądaj… albo wpisz ścieżkę. +MarketplaceDialog.EnvFile.NotFound.Header=Nie znaleziono pliku +MarketplaceDialog.EnvFile.NotFound.Message=Plik środowiska nie istnieje:\n{0} +MarketplaceDialog.Status.Installed=Zainstalowano +MarketplaceDialog.Status.Present=Obecny na dysku +MarketplaceDialog.Status.NotInstalled=Niezainstalowany +MarketplaceDialog.Status.RestartHint=Uruchom ponownie Hop po instalacji, zastosowaniu lub odinstalowaniu, aby przeładować wtyczki. Instalacja najpierw próbuje główne repozytorium, potem inne włączone. +MarketplaceDialog.Status.Installing=Instalowanie {0}... +MarketplaceDialog.Status.Validating=Walidacja pliku środowiska... +MarketplaceDialog.Status.Applying=Stosowanie pliku środowiska... +MarketplaceDialog.Status.Drift=Wykryto rozbieżność środowiska — użyj Zastosuj, aby naprawić +MarketplaceDialog.Error.Header=Błąd Marketplace +MarketplaceDialog.Error.ResolveHome=Nie można ustalić katalogu instalacji Hop. Uruchom Hop z katalogu głównego instalacji za pomocą launchera hop. +MarketplaceDialog.Install.Done.Header=Wtyczka zainstalowana +MarketplaceDialog.Install.Done.Message=Zainstalowano {0}.\n\nUruchom ponownie Hop, aby załadować wtyczkę. +MarketplaceDialog.Install.Error=Nie udało się zainstalować {0} +MarketplaceDialog.Uninstall.Confirm.Header=Odinstaluj wtyczkę +MarketplaceDialog.Uninstall.Confirm.Message=Usunąć wtyczkę zainstalowaną przez marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Nie można odinstalować +MarketplaceDialog.Uninstall.NoReceipt.Message=Brak potwierdzenia instalacji marketplace dla {0}. Tutaj można odinstalować tylko wtyczki zainstalowane przez marketplace. +MarketplaceDialog.Uninstall.Error=Nie udało się odinstalować {0} +MarketplaceDialog.Validate.Ok.Header=Środowisko OK +MarketplaceDialog.Validate.Ok.Message=Lokalna instalacja jest zgodna:\n{0} +MarketplaceDialog.Validate.Drift.Header=Rozbieżność środowiska +MarketplaceDialog.Validate.Drift.Message=Lokalna instalacja nie jest zgodna z plikiem środowiska: +MarketplaceDialog.Validate.Drift.Hint=Użyj Zastosuj, aby zainstalować brakujące wtyczki (lub poprawić wersje), a następnie uruchom ponownie Hop. +MarketplaceDialog.Validate.Error=Nie udało się zwalidować pliku środowiska +MarketplaceDialog.Apply.PruneConfirm.Header=Przyciąć wtyczki marketplace? +MarketplaceDialog.Apply.PruneConfirm.Message=Zastosowanie z przycinaniem odinstaluje wtyczki marketplace spoza pliku środowiska.\n\nKontynuować? +MarketplaceDialog.Apply.Disabled=Marketplace jest wyłączony w hop-config.json. +MarketplaceDialog.Apply.Done.Header=Środowisko zastosowane +MarketplaceDialog.Apply.Done.Message=Zastosowano:\n{0}\n\nUruchom ponownie Hop, aby załadować nowe wtyczki. +MarketplaceDialog.Apply.Error=Nie udało się zastosować pliku środowiska +ManageRepositoriesDialog.Shell.Title=Repozytoria Marketplace +ManageRepositoriesDialog.Help=Główne repozytorium jest próbowane pierwsze. Pozostałe włączone repozytoria to zapasowe.\nDomyślnie: Apache Repository (repository.apache.org), potem Maven Central.\nUstawienia są zapisywane w hop-config.json w '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Główne +ManageRepositoriesDialog.Column.Enabled=Włączone +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nazwa +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Dodaj +ManageRepositoriesDialog.Button.Edit=Edytuj +ManageRepositoriesDialog.Button.Remove=Usuń +ManageRepositoriesDialog.Button.SetPrimary=Ustaw jako główne +ManageRepositoriesDialog.Button.MoveUp=W górę +ManageRepositoriesDialog.Button.MoveDown=W dół +ManageRepositoriesDialog.Button.ResetDefaults=Przywróć domyślne +ManageRepositoriesDialog.Button.Save=Zapisz +ManageRepositoriesDialog.Button.Cancel=Anuluj +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repozytoria zapisane +ManageRepositoriesDialog.Save.Done.Message=Ustawienia repozytoriów Marketplace zostały zapisane w hop-config.json. +ManageRepositoriesDialog.Error.Header=Błąd repozytorium +ManageRepositoriesDialog.Error.Save=Nie można zapisać konfiguracji Marketplace +ManageRepositoriesDialog.Reset.Header=Resetuj repozytoria +ManageRepositoriesDialog.Reset.Message=Zastąpić listę repozytoriów Apache Repository (główne) i Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Dodaj repozytorium +ManageRepositoriesDialog.Edit.Title.Edit=Edytuj repozytorium +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nazwa +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Nazwa użytkownika +ManageRepositoriesDialog.Edit.Password=Hasło +ManageRepositoriesDialog.Edit.Enabled=Włączone (użyj w łańcuchu zapasowym) +ManageRepositoriesDialog.Edit.Primary=Główne (próbowane pierwsze) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id i URL są wymagane. +HopEnvironmentDialog.Shell.Title=Środowisko Marketplace — {0} +HopEnvironmentDialog.Untitled=Bez tytułu +HopEnvironmentDialog.Button.New=Nowy +HopEnvironmentDialog.Button.Open=Otwórz… +HopEnvironmentDialog.Button.Save=Zapisz +HopEnvironmentDialog.Button.SaveAs=Zapisz jako… +HopEnvironmentDialog.Button.Validate=Waliduj +HopEnvironmentDialog.Button.Validate.Tooltip=Sprawdź lokalną instalację względem tego pliku środowiska bez instalowania +HopEnvironmentDialog.Button.Apply=Zastosuj +HopEnvironmentDialog.Button.Apply.Tooltip=Zainstaluj wtyczki i zależności zadeklarowane w tym pliku środowiska +HopEnvironmentDialog.Button.Close=Zamknij +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Anuluj +HopEnvironmentDialog.Button.Add=Dodaj +HopEnvironmentDialog.Button.Edit=Edytuj +HopEnvironmentDialog.Button.Remove=Usuń +HopEnvironmentDialog.Button.MoveUp=W górę +HopEnvironmentDialog.Button.MoveDown=W dół +HopEnvironmentDialog.Button.AddFromCatalog=Z katalogu… +HopEnvironmentDialog.Button.ImportRepos=Importuj z hop-config… +HopEnvironmentDialog.Prune=Usuń dodatkowe przy zastosowaniu +HopEnvironmentDialog.Prune.Tooltip=Przy zastosowaniu odinstaluj wtyczki marketplace z potwierdzeniem spoza pliku +HopEnvironmentDialog.Strict=Ścisła walidacja +HopEnvironmentDialog.Strict.Tooltip=Przy walidacji niepowodzenie także, gdy zainstalowano dodatkowe wtyczki marketplace poza plikiem +HopEnvironmentDialog.Filter.Yaml=Środowisko YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Środowisko JSON (*.json) +HopEnvironmentDialog.Tab.General=Ogólne +HopEnvironmentDialog.Tab.Repositories=Repozytoria +HopEnvironmentDialog.Tab.Plugins=Wtyczki +HopEnvironmentDialog.Tab.Dependencies=Zależności +HopEnvironmentDialog.General.Help=Zadeklaruj, które wtyczki i jar-y ma mieć ta instalacja Hop. Zapisz jako hop-env.yaml na potrzeby CI/Docker lub użyj Waliduj / Zastosuj w tym oknie. +HopEnvironmentDialog.General.Version=Wersja specyfikacji +HopEnvironmentDialog.General.HopVersion=Wersja Hop +HopEnvironmentDialog.General.EnforceOnRun=Wymuś przy hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Gdy true, hop-run może zakończyć się niepowodzeniem, jeśli instalacja odbiega od tego pliku (także -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=Pierwszy wiersz jest główny (próbowany pierwszy); kolejne to zapasowe. Te repozytoria są w pliku środowiska (nie hop-config). Lepiej nie committować haseł. +HopEnvironmentDialog.Plugins.Help=Wtyczki marketplace do instalacji. Pozostaw Group ID puste, aby użyć org.apache.hop. Wersja może domyślnie pochodzić z wersji Hop na karcie Ogólne przy zastosowaniu. +HopEnvironmentDialog.Dependencies.Help=Opcjonalne jary Maven (np. sterowniki JDBC) instalowane w katalogu docelowym Hop home (domyślnie lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Nazwa użytkownika +HopEnvironmentDialog.Repo.Column.Password=Hasło +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Dodaj repozytorium +HopEnvironmentDialog.Repo.Edit.Edit=Edytuj repozytorium +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Wersja +HopEnvironmentDialog.Plugin.Edit.Add=Dodaj wtyczkę +HopEnvironmentDialog.Plugin.Edit.Edit=Edytuj wtyczkę +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Wersja +HopEnvironmentDialog.Dep.Column.Target=Cel +HopEnvironmentDialog.Dep.Edit.Add=Dodaj zależność +HopEnvironmentDialog.Dep.Edit.Edit=Edytuj zależność +HopEnvironmentDialog.Dirty.Header=Niezapisane zmiany +HopEnvironmentDialog.Dirty.Message=Zapisać zmiany w pliku środowiska przed kontynuacją? +HopEnvironmentDialog.SaveFirst.Header=Zapisać plik środowiska? +HopEnvironmentDialog.SaveFirst.Message=Zapisać plik środowiska przed Waliduj lub Zastosuj? +HopEnvironmentDialog.Validation.Header=Nieprawidłowe środowisko +HopEnvironmentDialog.Validation.RepoIdUrl=Każde repozytorium wymaga Id i URL. +HopEnvironmentDialog.Validation.PluginArtifact=Każda wtyczka wymaga Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Każda zależność wymaga Group ID, Artifact ID i Version. +HopEnvironmentDialog.ImportRepos.Header=Importuj repozytoria +HopEnvironmentDialog.ImportRepos.Message=Zastąpić listę repozytoriów włączonymi repozytoriami z hop-config.json? +HopEnvironmentDialog.Error.Header=Błąd edytora środowiska +HopEnvironmentDialog.Error.Load=Nie można wczytać pliku środowiska:\n{0} +HopEnvironmentDialog.Error.Save=Nie można zapisać pliku środowiska:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Nie można zaimportować repozytoriów z hop-config +AddPluginsFromCatalogDialog.Shell.Title=Dodaj wtyczki z katalogu +AddPluginsFromCatalogDialog.Filter.Label=Filtr +AddPluginsFromCatalogDialog.Button.Add=Dodaj zaznaczone +AddPluginsFromCatalogDialog.Button.Cancel=Anuluj +AddPluginsFromCatalogDialog.Column.Name=Nazwa +AddPluginsFromCatalogDialog.Column.Artifact=Artefakt +AddPluginsFromCatalogDialog.Column.Category=Kategoria +AddPluginsFromCatalogDialog.Column.Description=Opis diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_BR.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_BR.properties new file mode 100644 index 00000000000..79627241436 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_BR.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugins do Marketplace +MarketplaceLifecycleEnv.Help=O conjunto de plugins é definido em um arquivo hop-env. Estas opções controlam quando o Hop verifica esse arquivo para este ambiente de ciclo de vida (flexível em dev; rígido em produção). +MarketplaceLifecycleEnv.EnvFile.Label=Arquivo de ambiente +MarketplaceLifecycleEnv.Button.Browse=Procurar… +MarketplaceLifecycleEnv.Button.EditEnv=Editar… +MarketplaceLifecycleEnv.OnEnable.Label=Ao habilitar +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: sem verificação; warn: log/diálogo em caso de divergência; enforce: falhar ao habilitar o ambiente em caso de divergência +MarketplaceLifecycleEnv.Strict.Label=Rigoroso (falhar se houver plugins de marketplace extras instalados) +MarketplaceLifecycleEnv.Strict.Tooltip=Ao validar, também tratar como divergência plugins de marketplace com recibo que não estejam no arquivo de ambiente +MarketplaceLifecycleEnv.AutoApply.Label=Aplicar automaticamente plugins ausentes ao habilitar +MarketplaceLifecycleEnv.AutoApply.Tooltip=Quando habilitado, executa marketplace apply para plugins ausentes (apenas em máquinas controladas; padrão desligado) +MarketplaceDialog.Shell.Title=Marketplace de plugins +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Ambiente +MarketplaceDialog.Tab.Repositories=Repositórios +MarketplaceDialog.Repository.Label=Repositório principal +MarketplaceDialog.Repository.SetPrimary.Error=Não foi possível definir o repositório principal +MarketplaceDialog.Column.Name=Nome +MarketplaceDialog.Column.Artifact=Artefato +MarketplaceDialog.Column.Category=Categoria +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Descrição +MarketplaceDialog.Search.Label=Pesquisar +MarketplaceDialog.Search.Placeholder=Digite 3+ caracteres para filtrar plugins… +MarketplaceDialog.Search.Clear=Limpar filtro de pesquisa +MarketplaceDialog.Button.Install=Instalar +MarketplaceDialog.Button.Uninstall=Desinstalar +MarketplaceDialog.Button.Refresh=Atualizar +MarketplaceDialog.Button.Close=Fechar +MarketplaceDialog.Button.ManageRepos=Gerenciar… +MarketplaceDialog.Button.BrowseEnv=Procurar… +MarketplaceDialog.Button.EditEnv=Editar… +MarketplaceDialog.Button.EditEnv.Tooltip=Criar ou editar o arquivo de ambiente (repositórios, plugins, dependências) +MarketplaceDialog.Button.ValidateEnv=Validar +MarketplaceDialog.Button.ValidateEnv.Tooltip=Verificar a instalação local com o arquivo de ambiente sem instalar +MarketplaceDialog.Button.ApplyEnv=Aplicar +MarketplaceDialog.Button.ApplyEnv.Tooltip=Instalar plugins e dependências declarados no arquivo de ambiente +MarketplaceDialog.EnvFile.Label=Arquivo de ambiente +MarketplaceDialog.EnvFile.Prune=Remover extras ao aplicar +MarketplaceDialog.EnvFile.Prune.Tooltip=Ao aplicar, desinstalar plugins de marketplace com recibo que não estejam no arquivo +MarketplaceDialog.EnvFile.Strict=Validação rigorosa +MarketplaceDialog.EnvFile.Strict.Tooltip=Ao validar, também falhar se houver plugins de marketplace instalados além do arquivo +MarketplaceDialog.EnvFile.Filter.Env=Arquivos de ambiente Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Todos os arquivos +MarketplaceDialog.EnvFile.Missing.Header=Arquivo de ambiente obrigatório +MarketplaceDialog.EnvFile.Missing.Message=Selecione um hop-env.yaml (ou full-client-env.yaml) com Procurar…, ou digite o caminho. +MarketplaceDialog.EnvFile.NotFound.Header=Arquivo não encontrado +MarketplaceDialog.EnvFile.NotFound.Message=O arquivo de ambiente não existe:\n{0} +MarketplaceDialog.Status.Installed=Instalado +MarketplaceDialog.Status.Present=Presente no disco +MarketplaceDialog.Status.NotInstalled=Não instalado +MarketplaceDialog.Status.RestartHint=Reinicie o Hop após instalar, aplicar ou desinstalar para recarregar os plugins. A instalação tenta o repositório principal e depois outros repositórios habilitados. +MarketplaceDialog.Status.Installing=Instalando {0}... +MarketplaceDialog.Status.Validating=Validando arquivo de ambiente... +MarketplaceDialog.Status.Applying=Aplicando arquivo de ambiente... +MarketplaceDialog.Status.Drift=Divergência de ambiente detectada — use Aplicar para corrigir +MarketplaceDialog.Error.Header=Erro do Marketplace +MarketplaceDialog.Error.ResolveHome=Não foi possível determinar o diretório de instalação do Hop. Inicie o Hop a partir da raiz da instalação com o lançador hop. +MarketplaceDialog.Install.Done.Header=Plugin instalado +MarketplaceDialog.Install.Done.Message=Instalado {0}.\n\nReinicie o Hop para carregar o plugin. +MarketplaceDialog.Install.Error=Falha ao instalar {0} +MarketplaceDialog.Uninstall.Confirm.Header=Desinstalar plugin +MarketplaceDialog.Uninstall.Confirm.Message=Remover o plugin instalado pelo marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Não é possível desinstalar +MarketplaceDialog.Uninstall.NoReceipt.Message=Nenhum recibo de instalação do marketplace para {0}. Apenas plugins instalados pelo marketplace podem ser desinstalados aqui. +MarketplaceDialog.Uninstall.Error=Falha ao desinstalar {0} +MarketplaceDialog.Validate.Ok.Header=Ambiente OK +MarketplaceDialog.Validate.Ok.Message=A instalação local corresponde:\n{0} +MarketplaceDialog.Validate.Drift.Header=Divergência de ambiente +MarketplaceDialog.Validate.Drift.Message=A instalação local não corresponde ao arquivo de ambiente: +MarketplaceDialog.Validate.Drift.Hint=Use Aplicar para instalar plugins ausentes (ou corrigir versões) e reinicie o Hop. +MarketplaceDialog.Validate.Error=Falha ao validar o arquivo de ambiente +MarketplaceDialog.Apply.PruneConfirm.Header=Remover plugins de marketplace extras? +MarketplaceDialog.Apply.PruneConfirm.Message=Aplicar com remoção desinstalará plugins de marketplace que não estejam no arquivo de ambiente.\n\nContinuar? +MarketplaceDialog.Apply.Disabled=O Marketplace está desabilitado em hop-config.json. +MarketplaceDialog.Apply.Done.Header=Ambiente aplicado +MarketplaceDialog.Apply.Done.Message=Aplicado:\n{0}\n\nReinicie o Hop para carregar novos plugins. +MarketplaceDialog.Apply.Error=Falha ao aplicar o arquivo de ambiente +ManageRepositoriesDialog.Shell.Title=Repositórios do Marketplace +ManageRepositoriesDialog.Help=O repositório principal é tentado primeiro. Os demais repositórios habilitados são fallbacks.\nPadrões: Apache Repository (repository.apache.org) e depois Maven Central.\nAs configurações são salvas em hop-config.json em '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principal +ManageRepositoriesDialog.Column.Enabled=Habilitado +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nome +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Adicionar +ManageRepositoriesDialog.Button.Edit=Editar +ManageRepositoriesDialog.Button.Remove=Remover +ManageRepositoriesDialog.Button.SetPrimary=Definir como principal +ManageRepositoriesDialog.Button.MoveUp=Acima +ManageRepositoriesDialog.Button.MoveDown=Abaixo +ManageRepositoriesDialog.Button.ResetDefaults=Restaurar padrões +ManageRepositoriesDialog.Button.Save=Salvar +ManageRepositoriesDialog.Button.Cancel=Cancelar +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repositórios salvos +ManageRepositoriesDialog.Save.Done.Message=As configurações de repositórios do Marketplace foram salvas em hop-config.json. +ManageRepositoriesDialog.Error.Header=Erro de repositório +ManageRepositoriesDialog.Error.Save=Não foi possível salvar a configuração do Marketplace +ManageRepositoriesDialog.Reset.Header=Redefinir repositórios +ManageRepositoriesDialog.Reset.Message=Substituir a lista de repositórios por Apache Repository (principal) e Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Adicionar repositório +ManageRepositoriesDialog.Edit.Title.Edit=Editar repositório +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nome +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Nome de usuário +ManageRepositoriesDialog.Edit.Password=Senha +ManageRepositoriesDialog.Edit.Enabled=Habilitado (usar na cadeia de fallback) +ManageRepositoriesDialog.Edit.Primary=Principal (tentado primeiro) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id e URL são obrigatórios. +HopEnvironmentDialog.Shell.Title=Ambiente Marketplace — {0} +HopEnvironmentDialog.Untitled=Sem título +HopEnvironmentDialog.Button.New=Novo +HopEnvironmentDialog.Button.Open=Abrir… +HopEnvironmentDialog.Button.Save=Salvar +HopEnvironmentDialog.Button.SaveAs=Salvar como… +HopEnvironmentDialog.Button.Validate=Validar +HopEnvironmentDialog.Button.Validate.Tooltip=Verificar a instalação local com este arquivo de ambiente sem instalar +HopEnvironmentDialog.Button.Apply=Aplicar +HopEnvironmentDialog.Button.Apply.Tooltip=Instalar plugins e dependências declarados neste arquivo de ambiente +HopEnvironmentDialog.Button.Close=Fechar +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Cancelar +HopEnvironmentDialog.Button.Add=Adicionar +HopEnvironmentDialog.Button.Edit=Editar +HopEnvironmentDialog.Button.Remove=Remover +HopEnvironmentDialog.Button.MoveUp=Acima +HopEnvironmentDialog.Button.MoveDown=Abaixo +HopEnvironmentDialog.Button.AddFromCatalog=Do catálogo… +HopEnvironmentDialog.Button.ImportRepos=Importar do hop-config… +HopEnvironmentDialog.Prune=Remover extras ao aplicar +HopEnvironmentDialog.Prune.Tooltip=Ao aplicar, desinstalar plugins de marketplace com recibo que não estejam no arquivo +HopEnvironmentDialog.Strict=Validação rigorosa +HopEnvironmentDialog.Strict.Tooltip=Ao validar, também falhar se houver plugins de marketplace instalados além do arquivo +HopEnvironmentDialog.Filter.Yaml=Ambiente YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Ambiente JSON (*.json) +HopEnvironmentDialog.Tab.General=Geral +HopEnvironmentDialog.Tab.Repositories=Repositórios +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dependências +HopEnvironmentDialog.General.Help=Declare quais plugins e jars esta instalação Hop deve ter. Salve como hop-env.yaml para CI/Docker, ou use Validar / Aplicar neste diálogo. +HopEnvironmentDialog.General.Version=Versão da especificação +HopEnvironmentDialog.General.HopVersion=Versão do Hop +HopEnvironmentDialog.General.EnforceOnRun=Forçar no hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Se true, o hop-run pode falhar se a instalação divergir deste arquivo (também controlado por -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=A primeira linha é principal (tentada primeiro); as seguintes são fallbacks. Estes repositórios ficam no arquivo de ambiente (não hop-config). Prefira não fazer commit de senhas. +HopEnvironmentDialog.Plugins.Help=Plugins de marketplace a instalar. Deixe Group ID em branco para usar org.apache.hop. A versão pode vir da versão do Hop na aba Geral ao aplicar. +HopEnvironmentDialog.Dependencies.Help=Jars Maven opcionais (por exemplo drivers JDBC) instalados no diretório de destino do Hop home (padrão lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Nome de usuário +HopEnvironmentDialog.Repo.Column.Password=Senha +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Adicionar repositório +HopEnvironmentDialog.Repo.Edit.Edit=Editar repositório +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versão +HopEnvironmentDialog.Plugin.Edit.Add=Adicionar plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Editar plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versão +HopEnvironmentDialog.Dep.Column.Target=Destino +HopEnvironmentDialog.Dep.Edit.Add=Adicionar dependência +HopEnvironmentDialog.Dep.Edit.Edit=Editar dependência +HopEnvironmentDialog.Dirty.Header=Alterações não salvas +HopEnvironmentDialog.Dirty.Message=Salvar as alterações do arquivo de ambiente antes de continuar? +HopEnvironmentDialog.SaveFirst.Header=Salvar arquivo de ambiente? +HopEnvironmentDialog.SaveFirst.Message=Salvar o arquivo de ambiente antes de Validar ou Aplicar? +HopEnvironmentDialog.Validation.Header=Ambiente inválido +HopEnvironmentDialog.Validation.RepoIdUrl=Cada repositório precisa de um Id e uma URL. +HopEnvironmentDialog.Validation.PluginArtifact=Cada plugin precisa de um Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Cada dependência precisa de Group ID, Artifact ID e Version. +HopEnvironmentDialog.ImportRepos.Header=Importar repositórios +HopEnvironmentDialog.ImportRepos.Message=Substituir a lista de repositórios pelos repositórios habilitados de hop-config.json? +HopEnvironmentDialog.Error.Header=Erro do editor de ambiente +HopEnvironmentDialog.Error.Load=Não foi possível carregar o arquivo de ambiente:\n{0} +HopEnvironmentDialog.Error.Save=Não foi possível salvar o arquivo de ambiente:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Não foi possível importar repositórios do hop-config +AddPluginsFromCatalogDialog.Shell.Title=Adicionar plugins do catálogo +AddPluginsFromCatalogDialog.Filter.Label=Filtro +AddPluginsFromCatalogDialog.Button.Add=Adicionar selecionados +AddPluginsFromCatalogDialog.Button.Cancel=Cancelar +AddPluginsFromCatalogDialog.Column.Name=Nome +AddPluginsFromCatalogDialog.Column.Artifact=Artefato +AddPluginsFromCatalogDialog.Column.Category=Categoria +AddPluginsFromCatalogDialog.Column.Description=Descrição diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_PT.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_PT.properties new file mode 100644 index 00000000000..b8ee7a19477 --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_pt_PT.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Plugins do Marketplace +MarketplaceLifecycleEnv.Help=O conjunto de plugins é definido num ficheiro hop-env. Estas opções controlam quando o Hop verifica esse ficheiro para este ambiente de ciclo de vida (flexível em dev; rigoroso em produção). +MarketplaceLifecycleEnv.EnvFile.Label=Ficheiro de ambiente +MarketplaceLifecycleEnv.Button.Browse=Procurar… +MarketplaceLifecycleEnv.Button.EditEnv=Editar… +MarketplaceLifecycleEnv.OnEnable.Label=Ao ativar +MarketplaceLifecycleEnv.OnEnable.Tooltip=off: sem verificação; warn: registo/diálogo em caso de divergência; enforce: falhar ao ativar o ambiente em caso de divergência +MarketplaceLifecycleEnv.Strict.Label=Rigoroso (falhar se existirem plugins de marketplace extra instalados) +MarketplaceLifecycleEnv.Strict.Tooltip=Ao validar, também tratar como divergência plugins de marketplace com recibo que não estejam no ficheiro de ambiente +MarketplaceLifecycleEnv.AutoApply.Label=Aplicar automaticamente plugins em falta ao ativar +MarketplaceLifecycleEnv.AutoApply.Tooltip=Quando ativado, executa marketplace apply para plugins em falta (apenas em máquinas controladas; predefinição desligado) +MarketplaceDialog.Shell.Title=Marketplace de plugins +MarketplaceDialog.Tab.Plugins=Plugins +MarketplaceDialog.Tab.Environment=Ambiente +MarketplaceDialog.Tab.Repositories=Repositórios +MarketplaceDialog.Repository.Label=Repositório principal +MarketplaceDialog.Repository.SetPrimary.Error=Não foi possível definir o repositório principal +MarketplaceDialog.Column.Name=Nome +MarketplaceDialog.Column.Artifact=Artefato +MarketplaceDialog.Column.Category=Categoria +MarketplaceDialog.Column.Status=Status +MarketplaceDialog.Column.Description=Descrição +MarketplaceDialog.Search.Label=Pesquisar +MarketplaceDialog.Search.Placeholder=Introduza 3+ caracteres para filtrar plugins… +MarketplaceDialog.Search.Clear=Limpar filtro de pesquisa +MarketplaceDialog.Button.Install=Instalar +MarketplaceDialog.Button.Uninstall=Desinstalar +MarketplaceDialog.Button.Refresh=Atualizar +MarketplaceDialog.Button.Close=Fechar +MarketplaceDialog.Button.ManageRepos=Gerir… +MarketplaceDialog.Button.BrowseEnv=Procurar… +MarketplaceDialog.Button.EditEnv=Editar… +MarketplaceDialog.Button.EditEnv.Tooltip=Criar ou editar o ficheiro de ambiente (repositórios, plugins, dependências) +MarketplaceDialog.Button.ValidateEnv=Validar +MarketplaceDialog.Button.ValidateEnv.Tooltip=Verificar a instalação local com o ficheiro de ambiente sem instalar +MarketplaceDialog.Button.ApplyEnv=Aplicar +MarketplaceDialog.Button.ApplyEnv.Tooltip=Instalar plugins e dependências declarados no ficheiro de ambiente +MarketplaceDialog.EnvFile.Label=Ficheiro de ambiente +MarketplaceDialog.EnvFile.Prune=Remover extras ao aplicar +MarketplaceDialog.EnvFile.Prune.Tooltip=Ao aplicar, desinstalar plugins de marketplace com recibo que não estejam no ficheiro +MarketplaceDialog.EnvFile.Strict=Validação rigorosa +MarketplaceDialog.EnvFile.Strict.Tooltip=Ao validar, também falhar se existirem plugins de marketplace instalados para além do ficheiro +MarketplaceDialog.EnvFile.Filter.Env=Ficheiros de ambiente Hop (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=Todos os ficheiros +MarketplaceDialog.EnvFile.Missing.Header=Ficheiro de ambiente obrigatório +MarketplaceDialog.EnvFile.Missing.Message=Selecione um hop-env.yaml (ou full-client-env.yaml) com Procurar…, ou introduza o caminho. +MarketplaceDialog.EnvFile.NotFound.Header=Ficheiro não encontrado +MarketplaceDialog.EnvFile.NotFound.Message=O ficheiro de ambiente não existe:\n{0} +MarketplaceDialog.Status.Installed=Instalado +MarketplaceDialog.Status.Present=Presente no disco +MarketplaceDialog.Status.NotInstalled=Não instalado +MarketplaceDialog.Status.RestartHint=Reinicie o Hop após instalar, aplicar ou desinstalar para recarregar os plugins. A instalação tenta o repositório principal e depois outros repositórios ativados. +MarketplaceDialog.Status.Installing=A instalar {0}... +MarketplaceDialog.Status.Validating=A validar ficheiro de ambiente... +MarketplaceDialog.Status.Applying=A aplicar ficheiro de ambiente... +MarketplaceDialog.Status.Drift=Divergência de ambiente detetada — use Aplicar para corrigir +MarketplaceDialog.Error.Header=Erro do Marketplace +MarketplaceDialog.Error.ResolveHome=Não foi possível determinar o diretório de instalação do Hop. Inicie o Hop a partir da raiz da instalação com o lançador hop. +MarketplaceDialog.Install.Done.Header=Plugin instalado +MarketplaceDialog.Install.Done.Message=Instalado {0}.\n\nReinicie o Hop para carregar o plugin. +MarketplaceDialog.Install.Error=Falha ao instalar {0} +MarketplaceDialog.Uninstall.Confirm.Header=Desinstalar plugin +MarketplaceDialog.Uninstall.Confirm.Message=Remover o plugin instalado pelo marketplace {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=Não é possível desinstalar +MarketplaceDialog.Uninstall.NoReceipt.Message=Nenhum recibo de instalação do marketplace para {0}. Apenas plugins instalados pelo marketplace podem ser desinstalados aqui. +MarketplaceDialog.Uninstall.Error=Falha ao desinstalar {0} +MarketplaceDialog.Validate.Ok.Header=Ambiente OK +MarketplaceDialog.Validate.Ok.Message=A instalação local corresponde:\n{0} +MarketplaceDialog.Validate.Drift.Header=Divergência de ambiente +MarketplaceDialog.Validate.Drift.Message=A instalação local não corresponde ao ficheiro de ambiente: +MarketplaceDialog.Validate.Drift.Hint=Use Aplicar para instalar plugins em falta (ou corrigir versões) e reinicie o Hop. +MarketplaceDialog.Validate.Error=Falha ao validar o ficheiro de ambiente +MarketplaceDialog.Apply.PruneConfirm.Header=Remover plugins de marketplace extra? +MarketplaceDialog.Apply.PruneConfirm.Message=Aplicar com remoção desinstalará plugins de marketplace que não estejam no ficheiro de ambiente.\n\nContinuar? +MarketplaceDialog.Apply.Disabled=O Marketplace está desativado em hop-config.json. +MarketplaceDialog.Apply.Done.Header=Ambiente aplicado +MarketplaceDialog.Apply.Done.Message=Aplicado:\n{0}\n\nReinicie o Hop para carregar novos plugins. +MarketplaceDialog.Apply.Error=Falha ao aplicar o ficheiro de ambiente +ManageRepositoriesDialog.Shell.Title=Repositórios do Marketplace +ManageRepositoriesDialog.Help=O repositório principal é tentado primeiro. Os restantes repositórios ativados são fallbacks.\nPredefinições: Apache Repository (repository.apache.org) e depois Maven Central.\nAs definições são guardadas em hop-config.json em '${HOP_CONFIG_FOLDER}'. +ManageRepositoriesDialog.Column.Primary=Principal +ManageRepositoriesDialog.Column.Enabled=Habilitado +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=Nome +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=Auth +ManageRepositoriesDialog.Button.Add=Adicionar +ManageRepositoriesDialog.Button.Edit=Editar +ManageRepositoriesDialog.Button.Remove=Remover +ManageRepositoriesDialog.Button.SetPrimary=Definir como principal +ManageRepositoriesDialog.Button.MoveUp=Acima +ManageRepositoriesDialog.Button.MoveDown=Abaixo +ManageRepositoriesDialog.Button.ResetDefaults=Repor predefinições +ManageRepositoriesDialog.Button.Save=Guardar +ManageRepositoriesDialog.Button.Cancel=Cancelar +ManageRepositoriesDialog.Button.Ok=OK +ManageRepositoriesDialog.Save.Done.Header=Repositórios guardados +ManageRepositoriesDialog.Save.Done.Message=As definições de repositórios do Marketplace foram guardadas em hop-config.json. +ManageRepositoriesDialog.Error.Header=Erro de repositório +ManageRepositoriesDialog.Error.Save=Não foi possível guardar a configuração do Marketplace +ManageRepositoriesDialog.Reset.Header=Repor repositórios +ManageRepositoriesDialog.Reset.Message=Substituir a lista de repositórios por Apache Repository (principal) e Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=Adicionar repositório +ManageRepositoriesDialog.Edit.Title.Edit=Editar repositório +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=Nome +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=Nome de utilizador +ManageRepositoriesDialog.Edit.Password=Palavra-passe +ManageRepositoriesDialog.Edit.Enabled=Ativado (usar na cadeia de fallback) +ManageRepositoriesDialog.Edit.Primary=Principal (tentado primeiro) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id e URL são obrigatórios. +HopEnvironmentDialog.Shell.Title=Ambiente Marketplace — {0} +HopEnvironmentDialog.Untitled=Sem título +HopEnvironmentDialog.Button.New=Novo +HopEnvironmentDialog.Button.Open=Abrir… +HopEnvironmentDialog.Button.Save=Guardar +HopEnvironmentDialog.Button.SaveAs=Guardar como… +HopEnvironmentDialog.Button.Validate=Validar +HopEnvironmentDialog.Button.Validate.Tooltip=Verificar a instalação local com este ficheiro de ambiente sem instalar +HopEnvironmentDialog.Button.Apply=Aplicar +HopEnvironmentDialog.Button.Apply.Tooltip=Instalar plugins e dependências declarados neste ficheiro de ambiente +HopEnvironmentDialog.Button.Close=Fechar +HopEnvironmentDialog.Button.Ok=OK +HopEnvironmentDialog.Button.Cancel=Cancelar +HopEnvironmentDialog.Button.Add=Adicionar +HopEnvironmentDialog.Button.Edit=Editar +HopEnvironmentDialog.Button.Remove=Remover +HopEnvironmentDialog.Button.MoveUp=Acima +HopEnvironmentDialog.Button.MoveDown=Abaixo +HopEnvironmentDialog.Button.AddFromCatalog=Do catálogo… +HopEnvironmentDialog.Button.ImportRepos=Importar do hop-config… +HopEnvironmentDialog.Prune=Remover extras ao aplicar +HopEnvironmentDialog.Prune.Tooltip=Ao aplicar, desinstalar plugins de marketplace com recibo que não estejam no ficheiro +HopEnvironmentDialog.Strict=Validação rigorosa +HopEnvironmentDialog.Strict.Tooltip=Ao validar, também falhar se existirem plugins de marketplace instalados para além do ficheiro +HopEnvironmentDialog.Filter.Yaml=Ambiente YAML (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=Ambiente JSON (*.json) +HopEnvironmentDialog.Tab.General=Geral +HopEnvironmentDialog.Tab.Repositories=Repositórios +HopEnvironmentDialog.Tab.Plugins=Plugins +HopEnvironmentDialog.Tab.Dependencies=Dependências +HopEnvironmentDialog.General.Help=Declare quais plugins e jars esta instalação Hop deve ter. Guarde como hop-env.yaml para CI/Docker, ou use Validar / Aplicar neste diálogo. +HopEnvironmentDialog.General.Version=Versão da especificação +HopEnvironmentDialog.General.HopVersion=Versão do Hop +HopEnvironmentDialog.General.EnforceOnRun=Forçar no hop-run +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=Se true, o hop-run pode falhar se a instalação divergir deste ficheiro (também controlado por -Dhop.env.enforce=true) +HopEnvironmentDialog.Repositories.Help=A primeira linha é principal (tentada primeiro); as seguintes são fallbacks. Estes repositórios ficam no ficheiro de ambiente (não hop-config). Prefira não fazer commit de palavras-passe. +HopEnvironmentDialog.Plugins.Help=Plugins de marketplace a instalar. Deixe Group ID em branco para usar org.apache.hop. A versão pode vir da versão do Hop no separador Geral ao aplicar. +HopEnvironmentDialog.Dependencies.Help=Jars Maven opcionais (por exemplo controladores JDBC) instalados no diretório de destino do Hop home (predefinição lib/jdbc). +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=Nome de utilizador +HopEnvironmentDialog.Repo.Column.Password=Palavra-passe +HopEnvironmentDialog.Repo.Column.Auth=Auth +HopEnvironmentDialog.Repo.Edit.Add=Adicionar repositório +HopEnvironmentDialog.Repo.Edit.Edit=Editar repositório +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=Versão +HopEnvironmentDialog.Plugin.Edit.Add=Adicionar plugin +HopEnvironmentDialog.Plugin.Edit.Edit=Editar plugin +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=Versão +HopEnvironmentDialog.Dep.Column.Target=Destino +HopEnvironmentDialog.Dep.Edit.Add=Adicionar dependência +HopEnvironmentDialog.Dep.Edit.Edit=Editar dependência +HopEnvironmentDialog.Dirty.Header=Alterações não guardadas +HopEnvironmentDialog.Dirty.Message=Guardar as alterações do ficheiro de ambiente antes de continuar? +HopEnvironmentDialog.SaveFirst.Header=Guardar ficheiro de ambiente? +HopEnvironmentDialog.SaveFirst.Message=Guardar o ficheiro de ambiente antes de Validar ou Aplicar? +HopEnvironmentDialog.Validation.Header=Ambiente inválido +HopEnvironmentDialog.Validation.RepoIdUrl=Cada repositório precisa de um Id e um URL. +HopEnvironmentDialog.Validation.PluginArtifact=Cada plugin precisa de um Artifact ID. +HopEnvironmentDialog.Validation.DepGav=Cada dependência precisa de Group ID, Artifact ID e Version. +HopEnvironmentDialog.ImportRepos.Header=Importar repositórios +HopEnvironmentDialog.ImportRepos.Message=Substituir a lista de repositórios pelos repositórios ativados de hop-config.json? +HopEnvironmentDialog.Error.Header=Erro do editor de ambiente +HopEnvironmentDialog.Error.Load=Não foi possível carregar o ficheiro de ambiente:\n{0} +HopEnvironmentDialog.Error.Save=Não foi possível guardar o ficheiro de ambiente:\n{0} +HopEnvironmentDialog.Error.ImportRepos=Não foi possível importar repositórios do hop-config +AddPluginsFromCatalogDialog.Shell.Title=Adicionar plugins do catálogo +AddPluginsFromCatalogDialog.Filter.Label=Filtro +AddPluginsFromCatalogDialog.Button.Add=Adicionar selecionados +AddPluginsFromCatalogDialog.Button.Cancel=Cancelar +AddPluginsFromCatalogDialog.Column.Name=Nome +AddPluginsFromCatalogDialog.Column.Artifact=Artefato +AddPluginsFromCatalogDialog.Column.Category=Categoria +AddPluginsFromCatalogDialog.Column.Description=Descrição diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_zh_CN.properties b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_zh_CN.properties new file mode 100644 index 00000000000..a19e48d2a6a --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/gui/messages/messages_zh_CN.properties @@ -0,0 +1,209 @@ +# +# 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. +# + +MarketplaceGuiPlugin.Menu.Marketplace.Text=Marketplace... +MarketplaceGuiPlugin.Toolbar.Marketplace.Tooltip=Marketplace… +MarketplaceLifecycleEnv.Tab.Plugins=Marketplace 插件 +MarketplaceLifecycleEnv.Help=插件集在 hop-env 文件中定义。这些选项控制 Hop 何时针对此生命周期环境检查该文件(开发可宽松;生产可强制)。 +MarketplaceLifecycleEnv.EnvFile.Label=环境文件 +MarketplaceLifecycleEnv.Button.Browse=浏览… +MarketplaceLifecycleEnv.Button.EditEnv=编辑… +MarketplaceLifecycleEnv.OnEnable.Label=启用时 +MarketplaceLifecycleEnv.OnEnable.Tooltip=off:不检查;warn:漂移时记录/对话框;enforce:漂移时使环境启用失败 +MarketplaceLifecycleEnv.Strict.Label=严格(若安装了额外的 marketplace 插件则失败) +MarketplaceLifecycleEnv.Strict.Tooltip=验证时,也将环境文件未列出但有安装回执的 marketplace 插件视为漂移 +MarketplaceLifecycleEnv.AutoApply.Label=启用时自动应用缺失的插件 +MarketplaceLifecycleEnv.AutoApply.Tooltip=启用后,对缺失插件运行 marketplace apply(仅用于受控机器;默认关闭) +MarketplaceDialog.Shell.Title=插件 Marketplace +MarketplaceDialog.Tab.Plugins=插件 +MarketplaceDialog.Tab.Environment=环境 +MarketplaceDialog.Tab.Repositories=仓库 +MarketplaceDialog.Repository.Label=主仓库 +MarketplaceDialog.Repository.SetPrimary.Error=无法设置主仓库 +MarketplaceDialog.Column.Name=名称 +MarketplaceDialog.Column.Artifact=构件 +MarketplaceDialog.Column.Category=类别 +MarketplaceDialog.Column.Status=状态 +MarketplaceDialog.Column.Description=描述 +MarketplaceDialog.Search.Label=搜索 +MarketplaceDialog.Search.Placeholder=输入 3 个以上字符以筛选插件… +MarketplaceDialog.Search.Clear=清除搜索筛选 +MarketplaceDialog.Button.Install=安装 +MarketplaceDialog.Button.Uninstall=卸载 +MarketplaceDialog.Button.Refresh=刷新 +MarketplaceDialog.Button.Close=关闭 +MarketplaceDialog.Button.ManageRepos=管理… +MarketplaceDialog.Button.BrowseEnv=浏览… +MarketplaceDialog.Button.EditEnv=编辑… +MarketplaceDialog.Button.EditEnv.Tooltip=创建或编辑环境文件(仓库、插件、依赖) +MarketplaceDialog.Button.ValidateEnv=验证 +MarketplaceDialog.Button.ValidateEnv.Tooltip=对照环境文件检查本地安装,不执行安装 +MarketplaceDialog.Button.ApplyEnv=应用 +MarketplaceDialog.Button.ApplyEnv.Tooltip=安装环境文件中声明的插件和依赖 +MarketplaceDialog.EnvFile.Label=环境文件 +MarketplaceDialog.EnvFile.Prune=应用时修剪多余项 +MarketplaceDialog.EnvFile.Prune.Tooltip=应用时卸载文件中未列出但有安装回执的 marketplace 插件 +MarketplaceDialog.EnvFile.Strict=严格验证 +MarketplaceDialog.EnvFile.Strict.Tooltip=验证时,若安装了文件之外的额外 marketplace 插件也失败 +MarketplaceDialog.EnvFile.Filter.Env=Hop 环境文件 (*.yaml, *.yml, *.json) +MarketplaceDialog.EnvFile.Filter.All=所有文件 +MarketplaceDialog.EnvFile.Missing.Header=需要环境文件 +MarketplaceDialog.EnvFile.Missing.Message=使用“浏览…”选择 hop-env.yaml(或 full-client-env.yaml),或输入路径。 +MarketplaceDialog.EnvFile.NotFound.Header=未找到文件 +MarketplaceDialog.EnvFile.NotFound.Message=环境文件不存在:\n{0} +MarketplaceDialog.Status.Installed=已安装 +MarketplaceDialog.Status.Present=磁盘上存在 +MarketplaceDialog.Status.NotInstalled=未安装 +MarketplaceDialog.Status.RestartHint=安装、应用或卸载后请重启 Hop 以重新加载插件。安装先尝试主仓库,再尝试其他已启用的仓库。 +MarketplaceDialog.Status.Installing=正在安装 {0}... +MarketplaceDialog.Status.Validating=正在验证环境文件... +MarketplaceDialog.Status.Applying=正在应用环境文件... +MarketplaceDialog.Status.Drift=检测到环境漂移 — 使用“应用”修复 +MarketplaceDialog.Error.Header=Marketplace 错误 +MarketplaceDialog.Error.ResolveHome=无法确定 Hop 安装目录。请从安装根目录使用 hop 启动器启动 Hop。 +MarketplaceDialog.Install.Done.Header=插件已安装 +MarketplaceDialog.Install.Done.Message=已安装 {0}。\n\n请重启 Hop 以加载插件。 +MarketplaceDialog.Install.Error=安装 {0} 失败 +MarketplaceDialog.Uninstall.Confirm.Header=卸载插件 +MarketplaceDialog.Uninstall.Confirm.Message=移除通过 marketplace 安装的插件 {0}? +MarketplaceDialog.Uninstall.NoReceipt.Header=无法卸载 +MarketplaceDialog.Uninstall.NoReceipt.Message=没有 {0} 的 marketplace 安装回执。此处只能卸载通过 marketplace 安装的插件。 +MarketplaceDialog.Uninstall.Error=卸载 {0} 失败 +MarketplaceDialog.Validate.Ok.Header=环境正常 +MarketplaceDialog.Validate.Ok.Message=本地安装匹配:\n{0} +MarketplaceDialog.Validate.Drift.Header=环境漂移 +MarketplaceDialog.Validate.Drift.Message=本地安装与环境文件不匹配: +MarketplaceDialog.Validate.Drift.Hint=使用“应用”安装缺失插件(或修正版本),然后重启 Hop。 +MarketplaceDialog.Validate.Error=验证环境文件失败 +MarketplaceDialog.Apply.PruneConfirm.Header=修剪 marketplace 插件? +MarketplaceDialog.Apply.PruneConfirm.Message=带修剪的应用将卸载环境文件中未列出的 marketplace 插件。\n\n是否继续? +MarketplaceDialog.Apply.Disabled=Marketplace 在 hop-config.json 中已禁用。 +MarketplaceDialog.Apply.Done.Header=环境已应用 +MarketplaceDialog.Apply.Done.Message=已应用:\n{0}\n\n请重启 Hop 以加载新插件。 +MarketplaceDialog.Apply.Error=应用环境文件失败 +ManageRepositoriesDialog.Shell.Title=Marketplace 仓库 +ManageRepositoriesDialog.Help=先尝试主仓库。其他已启用的仓库为回退。\n默认:Apache Repository (repository.apache.org),然后是 Maven Central。\n设置保存在 '${HOP_CONFIG_FOLDER}' 下的 hop-config.json。 +ManageRepositoriesDialog.Column.Primary=主 +ManageRepositoriesDialog.Column.Enabled=启用 +ManageRepositoriesDialog.Column.Id=Id +ManageRepositoriesDialog.Column.Name=名称 +ManageRepositoriesDialog.Column.Url=URL +ManageRepositoriesDialog.Column.Auth=认证 +ManageRepositoriesDialog.Button.Add=添加 +ManageRepositoriesDialog.Button.Edit=编辑 +ManageRepositoriesDialog.Button.Remove=删除 +ManageRepositoriesDialog.Button.SetPrimary=设为主仓库 +ManageRepositoriesDialog.Button.MoveUp=上移 +ManageRepositoriesDialog.Button.MoveDown=下移 +ManageRepositoriesDialog.Button.ResetDefaults=重置默认 +ManageRepositoriesDialog.Button.Save=保存 +ManageRepositoriesDialog.Button.Cancel=取消 +ManageRepositoriesDialog.Button.Ok=确定 +ManageRepositoriesDialog.Save.Done.Header=仓库已保存 +ManageRepositoriesDialog.Save.Done.Message=Marketplace 仓库设置已保存到 hop-config.json。 +ManageRepositoriesDialog.Error.Header=仓库错误 +ManageRepositoriesDialog.Error.Save=无法保存 Marketplace 配置 +ManageRepositoriesDialog.Reset.Header=重置仓库 +ManageRepositoriesDialog.Reset.Message=将仓库列表替换为 Apache Repository(主)和 Maven Central? +ManageRepositoriesDialog.Edit.Title.Add=添加仓库 +ManageRepositoriesDialog.Edit.Title.Edit=编辑仓库 +ManageRepositoriesDialog.Edit.Id=ID +ManageRepositoriesDialog.Edit.Name=名称 +ManageRepositoriesDialog.Edit.Url=URL +ManageRepositoriesDialog.Edit.Username=用户名 +ManageRepositoriesDialog.Edit.Password=密码 +ManageRepositoriesDialog.Edit.Enabled=已启用(用于回退链) +ManageRepositoriesDialog.Edit.Primary=主仓库(优先尝试) +ManageRepositoriesDialog.Edit.IdUrlRequired=Id 和 URL 为必填项。 +HopEnvironmentDialog.Shell.Title=Marketplace 环境 — {0} +HopEnvironmentDialog.Untitled=未命名 +HopEnvironmentDialog.Button.New=新建 +HopEnvironmentDialog.Button.Open=打开… +HopEnvironmentDialog.Button.Save=保存 +HopEnvironmentDialog.Button.SaveAs=另存为… +HopEnvironmentDialog.Button.Validate=验证 +HopEnvironmentDialog.Button.Validate.Tooltip=对照此环境文件检查本地安装,不执行安装 +HopEnvironmentDialog.Button.Apply=应用 +HopEnvironmentDialog.Button.Apply.Tooltip=安装此环境文件中声明的插件和依赖 +HopEnvironmentDialog.Button.Close=关闭 +HopEnvironmentDialog.Button.Ok=确定 +HopEnvironmentDialog.Button.Cancel=取消 +HopEnvironmentDialog.Button.Add=添加 +HopEnvironmentDialog.Button.Edit=编辑 +HopEnvironmentDialog.Button.Remove=删除 +HopEnvironmentDialog.Button.MoveUp=上移 +HopEnvironmentDialog.Button.MoveDown=下移 +HopEnvironmentDialog.Button.AddFromCatalog=从目录… +HopEnvironmentDialog.Button.ImportRepos=从 hop-config 导入… +HopEnvironmentDialog.Prune=应用时修剪多余项 +HopEnvironmentDialog.Prune.Tooltip=应用时卸载文件中未列出但有安装回执的 marketplace 插件 +HopEnvironmentDialog.Strict=严格验证 +HopEnvironmentDialog.Strict.Tooltip=验证时,若安装了文件之外的额外 marketplace 插件也失败 +HopEnvironmentDialog.Filter.Yaml=YAML 环境 (*.yaml, *.yml) +HopEnvironmentDialog.Filter.Json=JSON 环境 (*.json) +HopEnvironmentDialog.Tab.General=常规 +HopEnvironmentDialog.Tab.Repositories=仓库 +HopEnvironmentDialog.Tab.Plugins=插件 +HopEnvironmentDialog.Tab.Dependencies=依赖 +HopEnvironmentDialog.General.Help=声明此 Hop 安装应具备的插件与 jar。保存为 hop-env.yaml 供 CI/Docker 使用,或在此对话框中使用验证/应用。 +HopEnvironmentDialog.General.Version=规范版本 +HopEnvironmentDialog.General.HopVersion=Hop 版本 +HopEnvironmentDialog.General.EnforceOnRun=在 hop-run 时强制 +HopEnvironmentDialog.General.EnforceOnRun.Tooltip=为 true 时,若安装偏离此文件,hop-run 可能失败(也可由 -Dhop.env.enforce=true 控制) +HopEnvironmentDialog.Repositories.Help=第一行为主(优先尝试);后续行为回退。这些仓库位于环境文件中(非 hop-config)。请尽量不要提交密码。 +HopEnvironmentDialog.Plugins.Help=要安装的 Marketplace 插件。Group ID 留空则使用 org.apache.hop。应用时版本可默认取自“常规”页的 Hop 版本。 +HopEnvironmentDialog.Dependencies.Help=可选 Maven jar(例如 JDBC 驱动),安装到 Hop home 下的目标目录(默认 lib/jdbc)。 +HopEnvironmentDialog.Repo.Column.Id=Id +HopEnvironmentDialog.Repo.Column.Url=URL +HopEnvironmentDialog.Repo.Column.Username=用户名 +HopEnvironmentDialog.Repo.Column.Password=密码 +HopEnvironmentDialog.Repo.Column.Auth=认证 +HopEnvironmentDialog.Repo.Edit.Add=添加仓库 +HopEnvironmentDialog.Repo.Edit.Edit=编辑仓库 +HopEnvironmentDialog.Plugin.Column.GroupId=Group ID +HopEnvironmentDialog.Plugin.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Plugin.Column.Version=版本 +HopEnvironmentDialog.Plugin.Edit.Add=添加插件 +HopEnvironmentDialog.Plugin.Edit.Edit=编辑插件 +HopEnvironmentDialog.Dep.Column.GroupId=Group ID +HopEnvironmentDialog.Dep.Column.ArtifactId=Artifact ID +HopEnvironmentDialog.Dep.Column.Version=版本 +HopEnvironmentDialog.Dep.Column.Target=目标 +HopEnvironmentDialog.Dep.Edit.Add=添加依赖 +HopEnvironmentDialog.Dep.Edit.Edit=编辑依赖 +HopEnvironmentDialog.Dirty.Header=未保存的更改 +HopEnvironmentDialog.Dirty.Message=继续前是否保存对环境文件的更改? +HopEnvironmentDialog.SaveFirst.Header=保存环境文件? +HopEnvironmentDialog.SaveFirst.Message=在验证或应用之前保存环境文件? +HopEnvironmentDialog.Validation.Header=无效环境 +HopEnvironmentDialog.Validation.RepoIdUrl=每个仓库需要 Id 和 URL。 +HopEnvironmentDialog.Validation.PluginArtifact=每个插件需要 Artifact ID。 +HopEnvironmentDialog.Validation.DepGav=每个依赖需要 Group ID、Artifact ID 和 Version。 +HopEnvironmentDialog.ImportRepos.Header=导入仓库 +HopEnvironmentDialog.ImportRepos.Message=用 hop-config.json 中已启用的仓库替换仓库列表? +HopEnvironmentDialog.Error.Header=环境编辑器错误 +HopEnvironmentDialog.Error.Load=无法加载环境文件:\n{0} +HopEnvironmentDialog.Error.Save=无法保存环境文件:\n{0} +HopEnvironmentDialog.Error.ImportRepos=无法从 hop-config 导入仓库 +AddPluginsFromCatalogDialog.Shell.Title=从目录添加插件 +AddPluginsFromCatalogDialog.Filter.Label=筛选 +AddPluginsFromCatalogDialog.Button.Add=添加所选 +AddPluginsFromCatalogDialog.Button.Cancel=取消 +AddPluginsFromCatalogDialog.Column.Name=名称 +AddPluginsFromCatalogDialog.Column.Artifact=构件 +AddPluginsFromCatalogDialog.Column.Category=类别 +AddPluginsFromCatalogDialog.Column.Description=描述 diff --git a/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml new file mode 100644 index 00000000000..f6a49b35d1a --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml @@ -0,0 +1,208 @@ +# 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. +# +# Source of truth for marketplace-optional plugins (not bundled in hop-client). +# full-client-env.yaml is generated from this list; GUI catalog loads it at runtime. +# When moving a plugin out of the fat assembly, add an entry here only. + +schemaVersion: "1" +groupId: org.apache.hop +plugins: + # + # Engines + # + - artifactId: hop-engines-spark + name: Native Spark engine + category: Engines + description: Run pipelines on Apache Spark (local or cluster). + installPath: plugins/engines/spark + modulePath: plugins/engines/spark + - artifactId: hop-engines-beam + name: Apache Beam engine + category: Engines + description: Beam pipeline engine and transforms; SDKs under plugins/engines/beam/lib-beam. + installPath: plugins/engines/beam + modulePath: plugins/engines/beam + # + # Transforms + # + - artifactId: hop-transform-script + name: Script + category: Transforms + description: Scripting transform with embedded language runtimes. + installPath: plugins/transforms/script + modulePath: plugins/transforms/script + - artifactId: hop-transform-tika + name: Apache Tika + category: Transforms + description: Extract text and metadata from documents via Apache Tika. + installPath: plugins/transforms/tika + modulePath: plugins/transforms/tika + - artifactId: hop-transform-drools + name: Drools + category: Transforms + description: Business rules with Drools. + installPath: plugins/transforms/drools + modulePath: plugins/transforms/drools + - artifactId: hop-transform-stanfordnlp + name: Stanford NLP + category: Transforms + description: Natural language processing with Stanford NLP. + installPath: plugins/transforms/stanfordnlp + modulePath: plugins/transforms/stanfordnlp + - artifactId: hop-transform-vcard + name: vCard + category: Transforms + description: vCard input/output + installPath: plugins/transforms/vcard + modulePath: plugins/transforms/vcard + - artifactId: hop-transform-fake + name: Fake + category: Transforms + description: Generate fake data + installPath: plugins/transforms/fake + modulePath: plugins/transforms/fake + # + # Technology + # + - artifactId: hop-tech-cassandra + name: Cassandra + category: Technology + description: Apache Cassandra input/output and related tech. + installPath: plugins/tech/cassandra + modulePath: plugins/tech/cassandra + - artifactId: hop-tech-parquet + name: Parquet + category: Technology + description: Parquet file format support. + installPath: plugins/tech/parquet + modulePath: plugins/tech/parquet + - artifactId: hop-tech-arrow + name: Apache Arrow + category: Technology + description: Apache Arrow integration. + installPath: plugins/tech/arrow + modulePath: plugins/tech/arrow + - artifactId: hop-tech-dropbox + name: Dropbox + category: Technology + description: Dropbox VFS and related support. + installPath: plugins/tech/dropbox + modulePath: plugins/tech/dropbox + - artifactId: hop-transform-edi2xml + name: EDI to XML + category: Transforms + description: Convert EDI messages to XML. + installPath: plugins/transforms/edi2xml + modulePath: plugins/transforms/edi2xml + - artifactId: hop-tech-neo4j + name: Neo4j + category: Technology + description: Neo4j Graph Database + installPath: plugins/tech/neo4j + modulePath: plugins/tech/neo4j + - artifactId: hop-tech-azure + name: Azure + category: Technology + description: Microsoft Azure Cloud platform + installPath: plugins/tech/azure + modulePath: plugins/tech/azure + - artifactId: hop-tech-aws + name: AWS + category: Technology + description: Amazon Web Services (AWS) + installPath: plugins/tech/aws + modulePath: plugins/tech/aws + - artifactId: hop-tech-mongodb + name: MongoDB + category: Technology + description: MongoDB + installPath: plugins/tech/mongodb + modulePath: plugins/tech/mongodb + - artifactId: hop-tech-elastic + name: Elastic + category: Technology + description: Elastic + installPath: plugins/tech/elastic + modulePath: plugins/tech/elastic + - artifactId: hop-tech-opensearch + name: OpenSearch + category: Technology + description: OpenSearch + installPath: plugins/tech/opensearch + modulePath: plugins/tech/opensearch + - artifactId: hop-tech-salesforce + name: SaleForce + category: Technology + description: SalesForce (SFDC) + installPath: plugins/tech/salesforce + modulePath: plugins/tech/salesforce + - artifactId: hop-tech-avro + name: Avro + category: Technology + description: Avro format + installPath: plugins/tech/avro + modulePath: plugins/tech/avro + - artifactId: hop-tech-google + name: Google + category: Google + description: Google technology + installPath: plugins/tech/google + modulePath: plugins/tech/google + - artifactId: hop-tech-minio + name: MinIO + category: Technology + description: MinIO + installPath: plugins/tech/minio + modulePath: plugins/tech/minio + - artifactId: hop-tech-databricks + name: Databricks + category: Technology + description: Databricks platform, VFS, Spark, jobs + installPath: plugins/tech/databricks + modulePath: plugins/tech/databricks + # + # Miscellaneous + # + - artifactId: hop-misc-py4j + name: PyHop + category: Miscellaneous + description: Hop for Python (PyHop) + installPath: plugins/misc/py4j + modulePath: plugins/misc/py4j + - artifactId: hop-misc-rest + name: REST + category: Miscellaneous + description: REST support + installPath: plugins/misc/rest + modulePath: plugins/misc/rest + - artifactId: hop-misc-documentation + name: Documentation + category: Miscellaneous + description: Generate Hop documentation + installPath: plugins/misc/documentation + modulePath: plugins/misc/documentation + - artifactId: hop-misc-import + name: Import + category: Miscellaneous + description: Import Kettle / Pentaho Data Integration metadata + installPath: plugins/misc/import + modulePath: plugins/misc/import + - artifactId: hop-misc-mail + name: Mail + category: Miscellaneous + description: Send / Receive mail + installPath: plugins/misc/mail + modulePath: plugins/misc/mail diff --git a/plugins/misc/marketplace/src/main/resources/version.xml b/plugins/misc/marketplace/src/main/resources/version.xml new file mode 100644 index 00000000000..36ab20e22eb --- /dev/null +++ b/plugins/misc/marketplace/src/main/resources/version.xml @@ -0,0 +1,20 @@ + + + +${project.version} diff --git a/plugins/misc/marketplace/src/main/samples/hop-env.example.yaml b/plugins/misc/marketplace/src/main/samples/hop-env.example.yaml new file mode 100644 index 00000000000..740c04ff62e --- /dev/null +++ b/plugins/misc/marketplace/src/main/samples/hop-env.example.yaml @@ -0,0 +1,53 @@ +# 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. + +# Example declarative Hop environment (copy to hop-env.yaml in your project or install). +# Apply with: +# hop marketplace apply -f hop-env.yaml +# hop marketplace apply -f hop-env.yaml --prune +# Validate: +# hop marketplace validate -f hop-env.yaml +# Optional hop-run enforcement: set enforceOnRun: true (or -Dhop.env.enforce=true) + +version: "1.0" +hopVersion: "2.19.0-SNAPSHOT" +enforceOnRun: false + +# First entry is primary; later entries are fallbacks. +repositories: + - id: asf + url: "https://repository.apache.org/content/groups/public/" + - id: central + url: "https://repo1.maven.org/maven2/" + # Local Sonatype Nexus for development (docker/marketplace-nexus/start.sh): + # - id: local-nexus + # url: "http://127.0.0.1:8081/repository/hop-plugins/" + +plugins: + - artifactId: hop-tech-parquet + version: "2.19.0-SNAPSHOT" + - artifactId: hop-engines-spark + version: "2.19.0-SNAPSHOT" + # Beam engine plugin only (Beam SDKs ship under plugins/engines/beam/lib-beam with the plugin): + # - artifactId: hop-engines-beam + # version: "2.19.0-SNAPSHOT" + +# Optional JDBC (or other) jars into lib/jdbc +# dependencies: +# - groupId: org.postgresql +# artifactId: postgresql +# version: "42.7.3" diff --git a/plugins/misc/marketplace/src/main/samples/hop-marketplace-catalog.example.yaml b/plugins/misc/marketplace/src/main/samples/hop-marketplace-catalog.example.yaml new file mode 100644 index 00000000000..06344a96e87 --- /dev/null +++ b/plugins/misc/marketplace/src/main/samples/hop-marketplace-catalog.example.yaml @@ -0,0 +1,22 @@ +# 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 +# +# Example remote catalog for marketplace discovery (host next to your Maven repo). +# Point catalogUrl in hop-marketplace-repo.yaml at this file's public URL. +# +schemaVersion: "1.0" +kind: hop-marketplace-catalog +repositoryId: data-hopper-community +plugins: + - groupId: org.apache.hop + artifactId: hop-datavault + version: 0.4.0-SNAPSHOT + name: Data Vault + category: Community + description: Data Vault modeling transforms for Apache Hop diff --git a/plugins/misc/marketplace/src/main/samples/hop-marketplace-repo.community.example.yaml b/plugins/misc/marketplace/src/main/samples/hop-marketplace-repo.community.example.yaml new file mode 100644 index 00000000000..6ad308be367 --- /dev/null +++ b/plugins/misc/marketplace/src/main/samples/hop-marketplace-repo.community.example.yaml @@ -0,0 +1,40 @@ +# 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 +# +# Shareable marketplace repository definition. +# +# ./hop marketplace repo import this-file.yaml +# ./hop marketplace query datavault +# ./hop marketplace install org.apache.hop:hop-datavault:0.4.0-SNAPSHOT +# +# browse: true → Hop lists *.zip from this Nexus repo live (no cache, no refresh). +# Optional plugins: metadata below only enriches display when present. +# +schemaVersion: "1.0" +kind: hop-marketplace-repository + +id: data-hopper-community +name: Data Hopper Community Plugins +url: https://repository.data-hopper.com/repository/hop-community-plugins/ +primary: false +enabled: true +browse: true +includeSnapshots: true + +# Optional metadata (enriches live Nexus list; not a cache) +# minHopVersion: only show when the running Hop is >= this version +# version: optional pin of the plugin artifact (else latest from browse) +plugins: + - groupId: org.apache.hop + artifactId: hop-datavault + version: 0.4.0-SNAPSHOT + minHopVersion: "2.18.1" + name: Data Vault + category: Community + description: Data Vault transforms for Hop diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalogTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalogTest.java new file mode 100644 index 00000000000..15ceb200908 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/OptionalPluginCatalogTest.java @@ -0,0 +1,70 @@ +/* + * 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.marketplace.catalog; + +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.util.List; +import org.junit.jupiter.api.Test; + +class OptionalPluginCatalogTest { + + @Test + void registryLoadsWave1Plugins() { + List plugins = OptionalPluginCatalog.listOptional(); + assertFalse(plugins.isEmpty()); + List ids = OptionalPluginCatalog.artifactIds(); + assertTrue(ids.contains("hop-tech-parquet")); + assertTrue(ids.contains("hop-engines-spark")); + assertTrue(ids.contains("hop-engines-beam")); + assertTrue(ids.contains("hop-transform-edi2xml")); + } + + @Test + void eachPluginHasInstallPath() { + for (OptionalPluginInfo info : OptionalPluginCatalog.listOptional()) { + assertTrue( + info.getInstallPath() != null && info.getInstallPath().startsWith("plugins/"), + "installPath for " + info.getArtifactId()); + } + } + + @Test + void queryFiltersCaseInsensitive() { + List parquet = OptionalPluginCatalog.query("parquet"); + assertFalse(parquet.isEmpty()); + assertTrue(parquet.stream().anyMatch(p -> "hop-tech-parquet".equals(p.getArtifactId()))); + + List engines = OptionalPluginCatalog.query("ENGINES"); + assertTrue(engines.stream().anyMatch(p -> "hop-engines-spark".equals(p.getArtifactId()))); + assertTrue(engines.stream().anyMatch(p -> "hop-engines-beam".equals(p.getArtifactId()))); + + List none = OptionalPluginCatalog.query("zzz-no-such-plugin-zzz"); + assertTrue(none.isEmpty()); + } + + @Test + void queryBlankReturnsAll() { + assertFalse(OptionalPluginCatalog.query("").isEmpty()); + assertFalse(OptionalPluginCatalog.query(null).isEmpty()); + assertEquals( + OptionalPluginCatalog.listOptional().size(), OptionalPluginCatalog.query(" ").size()); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryEnrichTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryEnrichTest.java new file mode 100644 index 00000000000..5f2a358fa23 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryEnrichTest.java @@ -0,0 +1,81 @@ +/* + * 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.marketplace.catalog; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.junit.jupiter.api.Test; + +class PluginDiscoveryEnrichTest { + + @Test + void enrichCategoryAndNameFromYamlWithoutGroupId() { + OptionalPluginInfo live = new OptionalPluginInfo(); + live.setGroupId("org.apache.hop"); + live.setArtifactId("hop-datavault"); + live.setVersion("0.4.0-SNAPSHOT"); + live.setName("hop-datavault"); + live.setCategory("auto-discovered"); + live.setDescription("org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/..."); + + OptionalPluginInfo meta = new OptionalPluginInfo(); + // no groupId — common in hand-written YAML + meta.setArtifactId("hop-datavault"); + meta.setName("Data Vault"); + meta.setCategory("Community"); + meta.setDescription("Data Vault transforms for Hop"); + meta.setMinHopVersion("2.18.1"); + meta.setVersion("0.4.0-SNAPSHOT"); + + MarketplaceRepository repo = new MarketplaceRepository("community", "https://example/repo/"); + repo.setPlugins(new ArrayList<>(List.of(meta))); + + List out = + PluginDiscovery.enrichWithDefinitionMetadata(List.of(live), repo); + assertEquals(1, out.size()); + assertEquals("Community", out.get(0).getCategory()); + assertEquals("Data Vault", out.get(0).getName()); + assertEquals("Data Vault transforms for Hop", out.get(0).getDescription()); + assertEquals("2.18.1", out.get(0).getMinHopVersion()); + } + + @Test + void enrichMatchesOnFullGa() { + OptionalPluginInfo live = new OptionalPluginInfo(); + live.setGroupId("org.apache.hop"); + live.setArtifactId("hop-datavault"); + live.setCategory("auto-discovered"); + live.setName("hop-datavault"); + + OptionalPluginInfo meta = new OptionalPluginInfo(); + meta.setGroupId("org.apache.hop"); + meta.setArtifactId("hop-datavault"); + meta.setCategory("Community"); + meta.setName("Data Vault"); + + MarketplaceRepository repo = new MarketplaceRepository("c", "https://x/"); + repo.setPlugins(List.of(meta)); + + PluginDiscovery.enrichWithDefinitionMetadata(List.of(live), repo); + assertEquals("Community", live.getCategory()); + assertEquals("Data Vault", live.getName()); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryResolveInstallTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryResolveInstallTest.java new file mode 100644 index 00000000000..1606d447743 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/PluginDiscoveryResolveInstallTest.java @@ -0,0 +1,83 @@ +/* + * 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.marketplace.catalog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class PluginDiscoveryResolveInstallTest { + + @Test + void pickExactArtifactId() throws Exception { + OptionalPluginInfo a = plugin("hop-datavault", "0.4.0-SNAPSHOT", "community"); + OptionalPluginInfo b = plugin("hop-other", "1.0", "community"); + OptionalPluginInfo chosen = PluginDiscovery.pickInstallMatch("hop-datavault", List.of(a, b)); + assertEquals("hop-datavault", chosen.getArtifactId()); + assertEquals("0.4.0-SNAPSHOT", chosen.getVersion()); + } + + @Test + void pickShortNameSubstringOnArtifact() throws Exception { + OptionalPluginInfo a = plugin("hop-datavault", "0.4.0-SNAPSHOT", "data-hopper-community"); + OptionalPluginInfo chosen = PluginDiscovery.pickInstallMatch("datavault", List.of(a)); + assertEquals("hop-datavault", chosen.getArtifactId()); + } + + @Test + void ambiguousShortNameThrows() { + OptionalPluginInfo a = plugin("hop-datavault", "0.4.0-SNAPSHOT", "c1"); + OptionalPluginInfo b = plugin("hop-datavault-ui", "1.0", "c1"); + HopException ex = + assertThrows( + HopException.class, () -> PluginDiscovery.pickInstallMatch("datavault", List.of(a, b))); + assertTrue(ex.getMessage().toLowerCase().contains("ambiguous")); + } + + @Test + void fullGavSkipsDiscovery() throws Exception { + PluginDiscovery.InstallTarget target = + PluginDiscovery.resolveInstall( + "org.example:foo:9.9.9", "org.apache.hop", "2.19.0-SNAPSHOT", null, null); + assertEquals("org.example:foo:9.9.9", target.coordinates().gav()); + assertNull(target.preferredRepoId()); + } + + @Test + void unknownNameFallsBackToLiteralParse() throws Exception { + PluginDiscovery.InstallTarget target = + PluginDiscovery.resolveInstall( + "totally-unknown-plugin", "org.apache.hop", "2.19.0-SNAPSHOT", null, null); + assertEquals( + "org.apache.hop:totally-unknown-plugin:2.19.0-SNAPSHOT", target.coordinates().gav()); + } + + private static OptionalPluginInfo plugin(String artifactId, String version, String source) { + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setGroupId("org.apache.hop"); + info.setArtifactId(artifactId); + info.setVersion(version); + info.setSource(source); + return info; + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/RemotePluginCatalogTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/RemotePluginCatalogTest.java new file mode 100644 index 00000000000..c223a05c4a4 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/RemotePluginCatalogTest.java @@ -0,0 +1,86 @@ +/* + * 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.marketplace.catalog; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class RemotePluginCatalogTest { + + @TempDir Path tempDir; + + @Test + void loadYamlCatalogAndFilter() throws Exception { + Path file = tempDir.resolve("catalog.yaml"); + Files.writeString( + file, + """ + schemaVersion: "1.0" + kind: hop-marketplace-catalog + plugins: + - groupId: org.apache.hop + artifactId: hop-datavault + version: 0.4.0-SNAPSHOT + name: Data Vault + category: Community + description: DV transforms + - groupId: com.example + artifactId: other + version: 1.0.0 + name: Other + """, + StandardCharsets.UTF_8); + + MarketplaceRepository repo = new MarketplaceRepository(); + repo.setId("community"); + repo.setCatalogUrl(file.toString()); + repo.setGroupIdFilter("org.apache.hop"); + repo.setIncludeSnapshots(true); + + List all = RemotePluginCatalog.load(repo); + assertEquals(1, all.size()); + assertEquals("hop-datavault", all.get(0).getArtifactId()); + assertEquals("community", all.get(0).getSource()); + + List filtered = RemotePluginCatalog.filter(all, repo, "vault"); + assertEquals(1, filtered.size()); + + repo.setIncludeSnapshots(false); + assertTrue(RemotePluginCatalog.filter(all, repo, null).isEmpty()); + } + + @Test + void nexusUrlParsing() { + assertEquals( + "hop-community-plugins", + NexusRepositoryBrowser.extractRepositoryName( + "https://repository.data-hopper.com/repository/hop-community-plugins/")); + assertEquals( + "https://repository.data-hopper.com", + NexusRepositoryBrowser.extractNexusBase( + "https://repository.data-hopper.com/repository/hop-community-plugins/")); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/VersionCompatTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/VersionCompatTest.java new file mode 100644 index 00000000000..e0b50568ba8 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/catalog/VersionCompatTest.java @@ -0,0 +1,87 @@ +/* + * 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.marketplace.catalog; + +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.util.List; +import org.junit.jupiter.api.Test; + +class VersionCompatTest { + + @Test + void compareNumericAndSnapshot() { + assertTrue(VersionCompat.compare("2.19.0", "2.18.1") > 0); + assertTrue(VersionCompat.compare("2.18.1", "2.19.0-SNAPSHOT") < 0); + assertTrue(VersionCompat.compare("0.4.0-SNAPSHOT", "0.3.0") > 0); + assertEquals(0, VersionCompat.compare("2.19.0-SNAPSHOT", "2.19.0-SNAPSHOT")); + } + + @Test + void latestPicksHighest() { + assertEquals( + "0.4.0-SNAPSHOT", VersionCompat.latest(List.of("0.3.0", "0.4.0-SNAPSHOT", "0.2.0"))); + } + + @Test + void minHopVersionFilter() { + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setArtifactId("hop-datavault"); + info.setMinHopVersion("2.18.1"); + assertTrue(VersionCompat.isCompatibleWithHop(info, "2.19.0-SNAPSHOT")); + assertTrue(VersionCompat.isCompatibleWithHop(info, "2.18.1")); + assertFalse(VersionCompat.isCompatibleWithHop(info, "2.17.0")); + } + + @Test + void maxHopVersionFilter() { + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setMaxHopVersion("2.19.0"); + assertTrue(VersionCompat.isCompatibleWithHop(info, "2.18.0")); + assertFalse(VersionCompat.isCompatibleWithHop(info, "2.20.0")); + } + + @Test + void noBoundsAlwaysCompatible() { + OptionalPluginInfo info = new OptionalPluginInfo(); + info.setArtifactId("auto"); + assertTrue(VersionCompat.isCompatibleWithHop(info, "2.19.0")); + assertTrue(VersionCompat.isCompatibleWithHop(info, null)); + } + + @Test + void nexusPreferableHigherVersion() { + OptionalPluginInfo existing = new OptionalPluginInfo(); + existing.setVersion("0.3.0"); + existing.setLastUpdated("2026-01-01T00:00:00Z"); + assertTrue( + NexusRepositoryBrowser.isPreferable("0.4.0-SNAPSHOT", "2026-07-21T14:00:00Z", existing)); + assertFalse(NexusRepositoryBrowser.isPreferable("0.2.0", "2026-07-21T14:00:00Z", existing)); + } + + @Test + void nexusPreferableSameVersionNewerTimestamp() { + OptionalPluginInfo existing = new OptionalPluginInfo(); + existing.setVersion("0.4.0-SNAPSHOT"); + existing.setLastUpdated("2026-07-21T10:56:17Z"); + assertTrue( + NexusRepositoryBrowser.isPreferable("0.4.0-SNAPSHOT", "2026-07-21T14:42:59Z", existing)); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/CliTableTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/CliTableTest.java new file mode 100644 index 00000000000..d0c72f6ed45 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/command/CliTableTest.java @@ -0,0 +1,94 @@ +/* + * 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.marketplace.command; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.jupiter.api.Test; + +class CliTableTest { + + @Test + void printTableBordersAlign() { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(buf, true, StandardCharsets.UTF_8); + CliTable.printTable(out, List.of("A", "BB"), List.of(List.of("1", "22"), List.of("xx", "y"))); + String text = buf.toString(StandardCharsets.UTF_8); + String[] lines = text.split("\\R"); + assertTrue(lines.length >= 5); + assertTrue(lines[0].startsWith("+")); + assertTrue(lines[0].endsWith("+")); + assertEquals(lines[0], lines[2]); + assertEquals(lines[0], lines[lines.length - 1]); + assertTrue(lines[1].startsWith("|")); + assertTrue(lines[1].contains("A")); + assertTrue(lines[1].contains("BB")); + } + + @Test + void csvEscapesCommaAndQuotes() { + assertEquals("plain", CliTable.escapeCsv("plain")); + assertEquals("\"a,b\"", CliTable.escapeCsv("a,b")); + assertEquals("\"say \"\"hi\"\"\"", CliTable.escapeCsv("say \"hi\"")); + assertEquals("\"a\nb\"", CliTable.escapeCsv("a\nb")); + } + + @Test + void printCsvHeaderOnlyWhenEmpty() { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(buf, true, StandardCharsets.UTF_8); + CliTable.printCsv(out, List.of("artifact", "version"), List.of()); + String text = buf.toString(StandardCharsets.UTF_8).trim(); + assertEquals("artifact,version", text); + } + + @Test + void printCsvRows() { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + PrintStream out = new PrintStream(buf, true, StandardCharsets.UTF_8); + CliTable.printCsv(out, List.of("artifact", "desc"), List.of(List.of("hop-x", "hello, world"))); + String text = buf.toString(StandardCharsets.UTF_8).trim(); + String[] lines = text.split("\\R"); + assertEquals(2, lines.length); + assertEquals("artifact,desc", lines[0]); + assertEquals("hop-x,\"hello, world\"", lines[1]); + } + + @Test + void truncateDescription() { + assertEquals("short", CliTable.truncate("short", 56)); + assertEquals("abcdefghij...", CliTable.truncate("abcdefghijklmnopqrstuvwxyz", 13)); + assertFalse(CliTable.truncate("line\nbreak", 20).contains("\n")); + } + + @Test + void queryHeadersOptionalGav() { + List without = MarketplaceCommand.QueryCommand.queryHeaders(false); + assertFalse(without.contains("GAV")); + assertTrue(without.contains("ARTIFACT")); + List with = MarketplaceCommand.QueryCommand.queryHeaders(true); + assertTrue(with.contains("GAV")); + assertEquals(without.size() + 1, with.size()); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceConfigTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceConfigTest.java new file mode 100644 index 00000000000..045fe323c03 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceConfigTest.java @@ -0,0 +1,103 @@ +/* + * 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.marketplace.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class MarketplaceConfigTest { + + @Test + void defaultsAreAsfPrimaryThenCentral() { + MarketplaceConfig config = new MarketplaceConfig(); + assertEquals(2, config.getRepositories().size()); + MarketplaceRepository primary = config.primaryRepository(); + assertEquals(MarketplaceConfig.DEFAULT_ASF_ID, primary.getId()); + assertTrue(primary.isPrimary()); + assertTrue(primary.normalizedUrl().contains("repository.apache.org")); + + List ordered = config.orderedRepositories(); + assertEquals(2, ordered.size()); + assertEquals(MarketplaceConfig.DEFAULT_ASF_ID, ordered.get(0).getId()); + assertEquals(MarketplaceConfig.DEFAULT_CENTRAL_ID, ordered.get(1).getId()); + } + + @Test + void ensureValidPrimaryPicksFirstWhenNoneMarked() { + MarketplaceConfig config = new MarketplaceConfig(); + config.getRepositories().forEach(r -> r.setPrimary(false)); + config.ensureValidPrimary(); + assertTrue(config.getRepositories().get(0).isPrimary()); + assertFalse(config.getRepositories().get(1).isPrimary()); + } + + @Test + void setPrimaryClearsOthers() throws Exception { + MarketplaceConfig config = new MarketplaceConfig(); + config.setPrimary(MarketplaceConfig.DEFAULT_CENTRAL_ID); + assertTrue(config.findRepository(MarketplaceConfig.DEFAULT_CENTRAL_ID).isPrimary()); + assertFalse(config.findRepository(MarketplaceConfig.DEFAULT_ASF_ID).isPrimary()); + assertEquals(MarketplaceConfig.DEFAULT_CENTRAL_ID, config.orderedRepositories().get(0).getId()); + } + + @Test + void addAndRemoveRepository() throws Exception { + MarketplaceConfig config = new MarketplaceConfig(); + config.addRepository( + new MarketplaceRepository( + "local", "Local Nexus", "http://127.0.0.1:8081/repository/hop-plugins/", true)); + assertEquals(3, config.getRepositories().size()); + assertEquals("local", config.primaryRepository().getId()); + config.removeRepository("local"); + assertEquals(2, config.getRepositories().size()); + assertTrue(config.primaryRepository().isPrimary()); + } + + @Test + void duplicateIdRejected() { + MarketplaceConfig config = new MarketplaceConfig(); + assertThrows( + Exception.class, + () -> + config.addRepository( + new MarketplaceRepository(MarketplaceConfig.DEFAULT_ASF_ID, "https://example/"))); + } + + @Test + void resetToDefaults() throws Exception { + MarketplaceConfig config = new MarketplaceConfig(); + config.addRepository(new MarketplaceRepository("x", "http://example.com/repo/", false)); + config.resetToDefaults(); + assertEquals(2, config.getRepositories().size()); + assertEquals(MarketplaceConfig.DEFAULT_ASF_ID, config.primaryRepository().getId()); + } + + @Test + void disabledReposSkippedInOrder() throws Exception { + MarketplaceConfig config = new MarketplaceConfig(); + config.setEnabled(MarketplaceConfig.DEFAULT_CENTRAL_ID, false); + List ordered = config.orderedRepositories(); + assertEquals(1, ordered.size()); + assertEquals(MarketplaceConfig.DEFAULT_ASF_ID, ordered.get(0).getId()); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinitionTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinitionTest.java new file mode 100644 index 00000000000..8c728dff598 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/config/MarketplaceRepositoryDefinitionTest.java @@ -0,0 +1,115 @@ +/* + * 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.marketplace.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class MarketplaceRepositoryDefinitionTest { + + @TempDir Path tempDir; + + @Test + void exportImportRoundTripOmitsPassword() throws Exception { + MarketplaceRepository repo = new MarketplaceRepository(); + repo.setId("community"); + repo.setName("Community"); + repo.setUrl("https://repository.example/repository/hop/"); + repo.setBrowse(true); + repo.setCatalogUrl("https://repository.example/catalog.yaml"); + repo.setIncludeSnapshots(true); + repo.setGroupIdFilter("com.example"); + repo.setSearchQuery("vault"); + repo.setPassword("secret"); + + Path file = tempDir.resolve("def.yaml"); + MarketplaceRepositoryDefinition.save(file, repo); + String text = Files.readString(file); + assertFalse(text.contains("secret")); + assertTrue(text.contains("browse: true")); + + MarketplaceRepository loaded = MarketplaceRepositoryDefinition.load(file); + assertEquals("community", loaded.getId()); + assertEquals("https://repository.example/repository/hop/", loaded.getUrl()); + assertTrue(loaded.isBrowse()); + assertEquals("https://repository.example/catalog.yaml", loaded.getCatalogUrl()); + assertEquals("com.example", loaded.getGroupIdFilter()); + assertEquals("vault", loaded.getSearchQuery()); + assertNull(loaded.getPassword()); + } + + @Test + void applyToConfigUpserts() throws Exception { + MarketplaceConfig config = new MarketplaceConfig(); + MarketplaceRepository first = new MarketplaceRepository("community", "https://a.example/r/"); + first.setBrowse(false); + config.addRepository(first); + + MarketplaceRepository imported = new MarketplaceRepository(); + imported.setId("community"); + imported.setUrl("https://b.example/r/"); + imported.setBrowse(true); + imported.setCatalogUrl("https://b.example/c.yaml"); + MarketplaceRepositoryDefinition.applyToConfig(config, imported, false); + + MarketplaceRepository found = config.findRepository("community"); + assertEquals("https://b.example/r/", found.getUrl()); + assertTrue(found.isBrowse()); + assertEquals("https://b.example/c.yaml", found.getCatalogUrl()); + } + + @Test + void importExportsEmbeddedPlugins() throws Exception { + Path file = tempDir.resolve("with-plugins.yaml"); + Files.writeString( + file, + """ + kind: hop-marketplace-repository + id: community + url: https://repository.example/repository/hop/ + browse: true + plugins: + - groupId: org.apache.hop + artifactId: hop-datavault + version: 0.4.0-SNAPSHOT + minHopVersion: "2.18.1" + category: auto-discovered + description: org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/ + lastUpdated: "2026-07-21T12:00:00.000+00:00" + """); + + MarketplaceRepository loaded = MarketplaceRepositoryDefinition.load(file); + assertEquals(1, loaded.getPlugins().size()); + assertEquals("hop-datavault", loaded.getPlugins().get(0).getArtifactId()); + assertEquals("auto-discovered", loaded.getPlugins().get(0).getCategory()); + assertEquals("2.18.1", loaded.getPlugins().get(0).getMinHopVersion()); + + Path out = tempDir.resolve("out.yaml"); + MarketplaceRepositoryDefinition.save(out, loaded); + String saved = Files.readString(out); + assertTrue(saved.contains("hop-datavault")); + assertTrue(saved.contains("minHopVersion")); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/EnvironmentApplierTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/EnvironmentApplierTest.java new file mode 100644 index 00000000000..69296565d80 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/EnvironmentApplierTest.java @@ -0,0 +1,120 @@ +/* + * 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.marketplace.env; + +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.json.HopJson; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.install.InstallReceipt; +import org.apache.hop.marketplace.install.PluginInstaller; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class EnvironmentApplierTest { + + @TempDir Path tempDir; + + @BeforeAll + static void initLogging() { + HopLogStore.init(); + } + + @Test + void validateDetectsMissingPlugin() throws Exception { + Path hopHome = tempDir.resolve("hop"); + Files.createDirectories(hopHome.resolve("plugins")); + + HopEnvironmentSpec env = new HopEnvironmentSpec(); + env.setHopVersion("2.19.0"); + HopEnvironmentSpec.PluginRef ref = new HopEnvironmentSpec.PluginRef(); + ref.setArtifactId("hop-tech-parquet"); + ref.setVersion("2.19.0"); + env.getPlugins().add(ref); + + EnvironmentDrift drift = + new EnvironmentApplier(new LogChannel("test"), hopHome, new MarketplaceConfig()) + .validate(env); + assertTrue(drift.hasDrift()); + assertEquals(1, drift.getMissingPlugins().size()); + } + + @Test + void validateOkWhenPluginOnDisk() throws Exception { + Path hopHome = tempDir.resolve("hop"); + Files.createDirectories(hopHome.resolve("plugins/tech/parquet")); + + HopEnvironmentSpec env = new HopEnvironmentSpec(); + HopEnvironmentSpec.PluginRef ref = new HopEnvironmentSpec.PluginRef(); + ref.setArtifactId("hop-tech-parquet"); + env.getPlugins().add(ref); + + EnvironmentDrift drift = + new EnvironmentApplier(new LogChannel("test"), hopHome, new MarketplaceConfig()) + .validate(env); + assertFalse(drift.hasDrift()); + } + + @Test + void validateVersionMismatchFromReceipt() throws Exception { + Path hopHome = tempDir.resolve("hop"); + Files.createDirectories(hopHome.resolve("plugins/tech/parquet")); + Files.createDirectories(hopHome.resolve(PluginInstaller.RECEIPTS_DIR)); + InstallReceipt receipt = new InstallReceipt(); + receipt.setGroupId("org.apache.hop"); + receipt.setArtifactId("hop-tech-parquet"); + receipt.setVersion("2.18.0"); + HopJson.newMapper() + .writeValue( + hopHome.resolve(PluginInstaller.RECEIPTS_DIR).resolve("hop-tech-parquet.json").toFile(), + receipt); + + HopEnvironmentSpec env = new HopEnvironmentSpec(); + HopEnvironmentSpec.PluginRef ref = new HopEnvironmentSpec.PluginRef(); + ref.setArtifactId("hop-tech-parquet"); + ref.setVersion("2.19.0"); + env.getPlugins().add(ref); + + EnvironmentDrift drift = + new EnvironmentApplier(new LogChannel("test"), hopHome, new MarketplaceConfig()) + .validate(env); + assertEquals(1, drift.getVersionMismatches().size()); + } + + @Test + void resolveEnvironmentFileDiscoversProjectFile() throws Exception { + Path project = tempDir.resolve("project"); + Files.createDirectories(project); + Path envFile = project.resolve("hop-env.yaml"); + Files.writeString(envFile, "version: \"1.0\"\n"); + System.setProperty("PROJECT_HOME", project.toString()); + try { + Path found = EnvironmentApplier.resolveEnvironmentFile(tempDir, null); + assertEquals(envFile.toAbsolutePath().normalize(), found); + } finally { + System.clearProperty("PROJECT_HOME"); + } + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/HopEnvironmentLoaderTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/HopEnvironmentLoaderTest.java new file mode 100644 index 00000000000..6ae387c25fe --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/HopEnvironmentLoaderTest.java @@ -0,0 +1,178 @@ +/* + * 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.marketplace.env; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HopEnvironmentLoaderTest { + + @TempDir Path tempDir; + + @Test + void loadYaml() throws Exception { + Path file = tempDir.resolve("hop-env.yaml"); + Files.writeString( + file, + """ + version: "1.0" + hopVersion: "2.19.0" + enforceOnRun: true + repositories: + - id: local + url: http://localhost:8082/artifactory/hop-plugins-local/ + plugins: + - artifactId: hop-tech-parquet + version: "2.19.0" + - artifactId: hop-engines-spark + dependencies: + - groupId: org.postgresql + artifactId: postgresql + version: "42.7.3" + """, + StandardCharsets.UTF_8); + + HopEnvironmentSpec env = HopEnvironmentLoader.load(file); + assertEquals("1.0", env.getVersion()); + assertEquals("2.19.0", env.getHopVersion()); + assertTrue(env.isEnforceOnRun()); + assertEquals(1, env.getRepositories().size()); + assertEquals("local", env.getRepositories().get(0).getId()); + assertEquals(2, env.getPlugins().size()); + assertEquals("hop-tech-parquet", env.getPlugins().get(0).getArtifactId()); + assertEquals(1, env.getDependencies().size()); + assertEquals("postgresql", env.getDependencies().get(0).getArtifactId()); + } + + @Test + void loadJson() throws Exception { + Path file = tempDir.resolve("hop-env.json"); + Files.writeString( + file, + """ + { + "version": "1.0", + "hopVersion": "2.19.0", + "enforceOnRun": false, + "plugins": [ { "artifactId": "hop-tech-arrow", "version": "2.19.0" } ] + } + """, + StandardCharsets.UTF_8); + HopEnvironmentSpec env = HopEnvironmentLoader.load(file); + assertFalse(env.isEnforceOnRun()); + assertEquals(1, env.getPlugins().size()); + assertEquals("hop-tech-arrow", env.getPlugins().get(0).getArtifactId()); + } + + @Test + void saveAndLoadYamlRoundTrip() throws Exception { + HopEnvironmentSpec spec = sampleSpec(); + Path file = tempDir.resolve("roundtrip.yaml"); + HopEnvironmentLoader.save(file, spec); + + String content = Files.readString(file, StandardCharsets.UTF_8); + assertTrue(content.contains("hopVersion:")); + assertTrue(content.contains("hop-tech-parquet")); + assertTrue(content.contains("postgresql")); + // blank groupId omitted + assertFalse(content.contains("groupId: org.apache.hop")); + + HopEnvironmentSpec loaded = HopEnvironmentLoader.load(file); + assertEquals("1.0", loaded.getVersion()); + assertEquals("2.19.0", loaded.getHopVersion()); + assertTrue(loaded.isEnforceOnRun()); + assertEquals(2, loaded.getRepositories().size()); + assertEquals("asf", loaded.getRepositories().get(0).getId()); + assertEquals("secret", loaded.getRepositories().get(1).getPassword()); + assertEquals(1, loaded.getPlugins().size()); + assertEquals("hop-tech-parquet", loaded.getPlugins().get(0).getArtifactId()); + assertNull(loaded.getPlugins().get(0).getGroupId()); + assertEquals(1, loaded.getDependencies().size()); + assertEquals("postgresql", loaded.getDependencies().get(0).getArtifactId()); + assertEquals("lib/jdbc", loaded.getDependencies().get(0).getTarget()); + } + + @Test + void saveAndLoadJsonRoundTrip() throws Exception { + HopEnvironmentSpec spec = sampleSpec(); + Path file = tempDir.resolve("roundtrip.json"); + HopEnvironmentLoader.save(file, spec); + + HopEnvironmentSpec loaded = HopEnvironmentLoader.load(file); + assertEquals(spec.getHopVersion(), loaded.getHopVersion()); + assertEquals(spec.getPlugins().size(), loaded.getPlugins().size()); + assertEquals( + spec.getPlugins().get(0).getArtifactId(), loaded.getPlugins().get(0).getArtifactId()); + assertEquals(spec.getDependencies().size(), loaded.getDependencies().size()); + } + + @Test + void saveMinimalOmitsEmptyLists() throws Exception { + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setVersion("1.0"); + spec.setHopVersion("2.19.0"); + Path file = tempDir.resolve("minimal.yaml"); + HopEnvironmentLoader.save(file, spec); + String content = Files.readString(file, StandardCharsets.UTF_8); + assertFalse(content.contains("repositories:")); + assertFalse(content.contains("plugins:")); + assertFalse(content.contains("dependencies:")); + HopEnvironmentSpec loaded = HopEnvironmentLoader.load(file); + assertEquals("2.19.0", loaded.getHopVersion()); + assertTrue(loaded.getPlugins().isEmpty()); + } + + private static HopEnvironmentSpec sampleSpec() { + HopEnvironmentSpec spec = new HopEnvironmentSpec(); + spec.setVersion("1.0"); + spec.setHopVersion("2.19.0"); + spec.setEnforceOnRun(true); + + HopEnvironmentSpec.RepositoryRef asf = new HopEnvironmentSpec.RepositoryRef(); + asf.setId("asf"); + asf.setUrl("https://repository.apache.org/content/groups/public/"); + HopEnvironmentSpec.RepositoryRef privateRepo = new HopEnvironmentSpec.RepositoryRef(); + privateRepo.setId("private"); + privateRepo.setUrl("https://nexus.example/repository/hop/"); + privateRepo.setUsername("ci"); + privateRepo.setPassword("secret"); + spec.getRepositories().add(asf); + spec.getRepositories().add(privateRepo); + + HopEnvironmentSpec.PluginRef plugin = new HopEnvironmentSpec.PluginRef(); + plugin.setArtifactId("hop-tech-parquet"); + plugin.setVersion("2.19.0"); + spec.getPlugins().add(plugin); + + HopEnvironmentSpec.DependencyRef dep = new HopEnvironmentSpec.DependencyRef(); + dep.setGroupId("org.postgresql"); + dep.setArtifactId("postgresql"); + dep.setVersion("42.7.3"); + dep.setTarget("lib/jdbc"); + spec.getDependencies().add(dep); + return spec; + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/MarketplaceAttributesTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/MarketplaceAttributesTest.java new file mode 100644 index 00000000000..a7e28d0e565 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/env/MarketplaceAttributesTest.java @@ -0,0 +1,68 @@ +/* + * 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.marketplace.env; + +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 org.apache.hop.core.AttributesContext; +import org.junit.jupiter.api.Test; + +class MarketplaceAttributesTest { + + @Test + void purposeDefaults() { + assertEquals( + MarketplaceAttributes.ON_ENABLE_ENFORCE, + MarketplaceAttributes.defaultOnEnableForPurpose("Production")); + assertEquals( + MarketplaceAttributes.ON_ENABLE_WARN, + MarketplaceAttributes.defaultOnEnableForPurpose("Testing")); + assertEquals( + MarketplaceAttributes.ON_ENABLE_WARN, + MarketplaceAttributes.defaultOnEnableForPurpose("Acceptance")); + assertEquals( + MarketplaceAttributes.ON_ENABLE_OFF, + MarketplaceAttributes.defaultOnEnableForPurpose("Development")); + assertEquals( + MarketplaceAttributes.ON_ENABLE_OFF, MarketplaceAttributes.defaultOnEnableForPurpose(null)); + } + + @Test + void explicitOverridesPurpose() { + AttributesContext ctx = new AttributesContext(); + ctx.setAttribute( + MarketplaceAttributes.GROUP, + MarketplaceAttributes.KEY_ON_ENABLE, + MarketplaceAttributes.ON_ENABLE_OFF); + assertEquals( + MarketplaceAttributes.ON_ENABLE_OFF, + MarketplaceAttributes.resolveOnEnable(ctx, "Production")); + } + + @Test + void strictAndAutoApply() { + AttributesContext ctx = new AttributesContext(); + assertFalse(MarketplaceAttributes.isStrict(ctx)); + ctx.setAttribute(MarketplaceAttributes.GROUP, MarketplaceAttributes.KEY_STRICT, "true"); + assertTrue(MarketplaceAttributes.isStrict(ctx)); + ctx.setAttribute(MarketplaceAttributes.GROUP, MarketplaceAttributes.KEY_AUTO_APPLY, "Y"); + assertTrue(MarketplaceAttributes.isAutoApply(ctx)); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/HopHomeTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/HopHomeTest.java new file mode 100644 index 00000000000..fae4b4a1840 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/HopHomeTest.java @@ -0,0 +1,69 @@ +/* + * 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.marketplace.install; + +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.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HopHomeTest { + + @TempDir Path tempDir; + + @Test + void isHopHomeRequiresPluginsDir() throws Exception { + Path hop = tempDir.resolve("hop"); + Files.createDirectories(hop); + assertFalse(HopHome.isHopHome(hop)); + Files.createDirectories(hop.resolve("plugins")); + assertTrue(HopHome.isHopHome(hop)); + } + + @Test + void resolveUsesUserDirWhenItIsHopHome() throws Exception { + Path hop = tempDir.resolve("hop"); + Files.createDirectories(hop.resolve("plugins")); + String previous = System.getProperty("user.dir"); + try { + System.setProperty("user.dir", hop.toString()); + assertEquals(hop.toAbsolutePath().normalize(), HopHome.resolve()); + } finally { + System.setProperty("user.dir", previous); + } + } + + @Test + void resolveFallsBackToParentOfUserDir() throws Exception { + Path hop = tempDir.resolve("hop"); + Files.createDirectories(hop.resolve("plugins")); + Path nested = hop.resolve("subdir"); + Files.createDirectories(nested); + String previous = System.getProperty("user.dir"); + try { + System.setProperty("user.dir", nested.toString()); + assertEquals(hop.toAbsolutePath().normalize(), HopHome.resolve()); + } finally { + System.setProperty("user.dir", previous); + } + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java new file mode 100644 index 00000000000..112bb7cae79 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/install/PluginInstallerTest.java @@ -0,0 +1,218 @@ +/* + * 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.marketplace.install; + +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 com.sun.net.httpserver.HttpServer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.marketplace.config.MarketplaceConfig; +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.apache.hop.marketplace.resolve.MavenCoordinates; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PluginInstallerTest { + + @TempDir Path tempDir; + + @BeforeAll + static void initLogging() { + HopLogStore.init(); + } + + @Test + void protectedPaths() { + assertFalse(PluginInstaller.isProtectedPath("lib/beam/foo.jar")); + assertFalse(PluginInstaller.isProtectedPath("plugins/engines/beam/lib-beam/x.jar")); + assertTrue(PluginInstaller.isProtectedPath("lib/core/bar.jar")); + assertFalse(PluginInstaller.isProtectedPath("plugins/engines/beam/hop.jar")); + assertFalse(PluginInstaller.isProtectedPath("plugins/tech/parquet/lib/x.jar")); + } + + @Test + void installAndUninstallFromLocalHttpRepo() throws Exception { + byte[] zipBytes = buildPluginZip(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + String path = "/org/apache/hop/hop-test-plugin/1.0.0/hop-test-plugin-1.0.0.zip"; + server.createContext( + path, + exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, zipBytes.length); + exchange.getResponseBody().write(zipBytes); + exchange.close(); + }); + server.start(); + try { + int port = server.getAddress().getPort(); + Path hopHome = tempDir.resolve("hop"); + Files.createDirectories(hopHome.resolve("plugins")); + + MarketplaceConfig config = new MarketplaceConfig(); + config.getRepositories().clear(); + MarketplaceRepository local = + new MarketplaceRepository("local", "http://127.0.0.1:" + port + "/", true); + config.getRepositories().add(local); + + LogChannel log = new LogChannel("test"); + PluginInstaller installer = new PluginInstaller(log, hopHome, config); + MavenCoordinates coords = new MavenCoordinates("org.apache.hop", "hop-test-plugin", "1.0.0"); + InstallReceipt receipt = installer.install(coords, true); + assertEquals("local", receipt.getRepositoryId()); + + Path pluginJar = hopHome.resolve("plugins/tech/test/plugin.jar"); + assertTrue(Files.isRegularFile(pluginJar)); + assertTrue( + Files.isRegularFile( + hopHome.resolve(PluginInstaller.RECEIPTS_DIR).resolve("hop-test-plugin.json"))); + // protected path from zip must not be written + assertFalse(Files.exists(hopHome.resolve("lib/core/evil.jar"))); + + new PluginUninstaller(log, hopHome).uninstall("hop-test-plugin"); + assertFalse(Files.exists(pluginJar)); + assertFalse( + Files.exists( + hopHome.resolve(PluginInstaller.RECEIPTS_DIR).resolve("hop-test-plugin.json"))); + } finally { + server.stop(0); + } + } + + @Test + void installWithBasicAuth() throws Exception { + byte[] zipBytes = buildPluginZip(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + String path = "/org/apache/hop/hop-test-plugin/1.0.0/hop-test-plugin-1.0.0.zip"; + server.createContext( + path, + exchange -> { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth == null || !auth.startsWith("Basic ")) { + exchange.sendResponseHeaders(401, -1); + exchange.close(); + return; + } + String decoded = + new String( + java.util.Base64.getDecoder().decode(auth.substring("Basic ".length())), + StandardCharsets.UTF_8); + if (!"admin:s3cret".equals(decoded)) { + exchange.sendResponseHeaders(401, -1); + exchange.close(); + return; + } + exchange.getResponseHeaders().add("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, zipBytes.length); + exchange.getResponseBody().write(zipBytes); + exchange.close(); + }); + server.start(); + try { + int port = server.getAddress().getPort(); + Path hopHome = tempDir.resolve("hop-auth"); + Files.createDirectories(hopHome.resolve("plugins")); + + MarketplaceConfig config = new MarketplaceConfig(); + config.getRepositories().clear(); + MarketplaceRepository local = + new MarketplaceRepository("local", "http://127.0.0.1:" + port + "/", "admin", "s3cret"); + local.setPrimary(true); + config.getRepositories().add(local); + + new PluginInstaller(new LogChannel("test"), hopHome, config) + .install(new MavenCoordinates("org.apache.hop", "hop-test-plugin", "1.0.0"), true); + assertTrue(Files.isRegularFile(hopHome.resolve("plugins/tech/test/plugin.jar"))); + } finally { + server.stop(0); + } + } + + @Test + void installFallsBackToSecondRepository() throws Exception { + byte[] zipBytes = buildPluginZip(); + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + String path = "/org/apache/hop/hop-test-plugin/1.0.0/hop-test-plugin-1.0.0.zip"; + server.createContext( + "/missing/", + exchange -> { + exchange.sendResponseHeaders(404, -1); + exchange.close(); + }); + server.createContext( + path, + exchange -> { + exchange.getResponseHeaders().add("Content-Type", "application/zip"); + exchange.sendResponseHeaders(200, zipBytes.length); + exchange.getResponseBody().write(zipBytes); + exchange.close(); + }); + server.start(); + try { + int port = server.getAddress().getPort(); + Path hopHome = tempDir.resolve("hop-fallback"); + Files.createDirectories(hopHome.resolve("plugins")); + + MarketplaceConfig config = new MarketplaceConfig(); + config.getRepositories().clear(); + MarketplaceRepository missing = + new MarketplaceRepository("missing", "http://127.0.0.1:" + port + "/missing/", true); + MarketplaceRepository ok = + new MarketplaceRepository("ok", "http://127.0.0.1:" + port + "/", false); + config.getRepositories().add(missing); + config.getRepositories().add(ok); + + InstallReceipt receipt = + new PluginInstaller(new LogChannel("test"), hopHome, config) + .install(new MavenCoordinates("org.apache.hop", "hop-test-plugin", "1.0.0"), true); + assertEquals("ok", receipt.getRepositoryId()); + assertTrue(Files.isRegularFile(hopHome.resolve("plugins/tech/test/plugin.jar"))); + } finally { + server.stop(0); + } + } + + private static byte[] buildPluginZip() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try (ZipOutputStream zos = new ZipOutputStream(bos)) { + zos.putNextEntry(new ZipEntry("plugins/tech/test/plugin.jar")); + zos.write("fake-jar".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + zos.putNextEntry(new ZipEntry("plugins/tech/test/version.xml")); + zos.write("1.0.0".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + // Should be ignored on activate + zos.putNextEntry(new ZipEntry("lib/core/evil.jar")); + zos.write("nope".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + return bos.toByteArray(); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenCoordinatesTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenCoordinatesTest.java new file mode 100644 index 00000000000..9918a0a8b36 --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenCoordinatesTest.java @@ -0,0 +1,68 @@ +/* + * 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.marketplace.resolve; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class MavenCoordinatesTest { + + @Test + void parseArtifactOnly() throws Exception { + MavenCoordinates c = MavenCoordinates.parse("hop-tech-parquet", "org.apache.hop", "2.19.0"); + assertEquals("org.apache.hop", c.groupId()); + assertEquals("hop-tech-parquet", c.artifactId()); + assertEquals("2.19.0", c.version()); + } + + @Test + void parseArtifactVersion() throws Exception { + MavenCoordinates c = + MavenCoordinates.parse("hop-engines-spark:2.19.0-SNAPSHOT", "org.apache.hop", "x"); + assertEquals("hop-engines-spark", c.artifactId()); + assertEquals("2.19.0-SNAPSHOT", c.version()); + } + + @Test + void parseFullGav() throws Exception { + MavenCoordinates c = MavenCoordinates.parse("org.example:foo:1.0", "ignored", "ignored"); + assertEquals("org.example", c.groupId()); + assertEquals("foo", c.artifactId()); + assertEquals("1.0", c.version()); + } + + @Test + void zipPath() { + MavenCoordinates c = new MavenCoordinates("org.apache.hop", "hop-tech-parquet", "2.19.0"); + assertEquals( + "org/apache/hop/hop-tech-parquet/2.19.0/hop-tech-parquet-2.19.0.zip", + c.zipRepositoryPath()); + } + + @Test + void missingVersionFails() { + HopException ex = + assertThrows( + HopException.class, () -> MavenCoordinates.parse("hop-tech-parquet", "g", null)); + assertTrue(ex.getMessage().toLowerCase().contains("version")); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenRepositoryClientSnapshotTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenRepositoryClientSnapshotTest.java new file mode 100644 index 00000000000..18d6c1f93bd --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/MavenRepositoryClientSnapshotTest.java @@ -0,0 +1,94 @@ +/* + * 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.marketplace.resolve; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.apache.hop.marketplace.config.MarketplaceRepository; +import org.junit.jupiter.api.Test; + +class MavenRepositoryClientSnapshotTest { + + @Test + void parseSnapshotZipFromMetadata() { + String xml = + """ + + 2.19.0-SNAPSHOT + + + 20260719.204953 + 1 + + + + zip + 2.19.0-20260719.204953-1 + 20260719204953 + + + pom + 2.19.0-20260719.204953-1 + + + + + """; + assertEquals( + "2.19.0-20260719.204953-1", + MavenRepositoryClient.parseSnapshotZipValue(xml, "hop-tech-parquet", "2.19.0-SNAPSHOT")); + } + + @Test + void parseSnapshotFromTimestampOnly() { + String xml = + """ + + + + 20260101.120000 + 3 + + + + """; + assertEquals( + "1.0.0-20260101.120000-3", + MavenRepositoryClient.parseSnapshotZipValue(xml, "x", "1.0.0-SNAPSHOT")); + } + + @Test + void emptyMetadata() { + assertNull(MavenRepositoryClient.parseSnapshotZipValue("", "a", "1-SNAPSHOT")); + } + + @Test + void uniqueSnapshotUsesBaseDirectory() throws Exception { + MavenRepositoryClient client = new MavenRepositoryClient(null); + MarketplaceRepository repo = + new MarketplaceRepository("x", "https://example.com/repository/hop/"); + String path = + client.resolveZipRelativePath( + repo, + new MavenCoordinates("org.apache.hop", "hop-datavault", "0.4.0-20260721.105615-1")); + assertEquals( + "org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/hop-datavault-0.4.0-20260721.105615-1.zip", + path); + } +} diff --git a/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/SnapshotVersionsTest.java b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/SnapshotVersionsTest.java new file mode 100644 index 00000000000..3c8a6dba4ba --- /dev/null +++ b/plugins/misc/marketplace/src/test/java/org/apache/hop/marketplace/resolve/SnapshotVersionsTest.java @@ -0,0 +1,65 @@ +/* + * 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.marketplace.resolve; + +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 org.junit.jupiter.api.Test; + +class SnapshotVersionsTest { + + @Test + void uniqueToBase() { + assertEquals("0.4.0-SNAPSHOT", SnapshotVersions.toBaseVersion("0.4.0-20260721.105615-1")); + assertEquals("2.19.0-SNAPSHOT", SnapshotVersions.toBaseVersion("2.19.0-20260719.204953-1")); + } + + @Test + void alreadyBaseOrRelease() { + assertEquals("0.4.0-SNAPSHOT", SnapshotVersions.toBaseVersion("0.4.0-SNAPSHOT")); + assertEquals("1.2.3", SnapshotVersions.toBaseVersion("1.2.3")); + } + + @Test + void preferPathFolder() { + assertEquals( + "0.4.0-SNAPSHOT", + SnapshotVersions.toBaseVersion( + "0.4.0-20260721.105615-1", + "/org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/hop-datavault-0.4.0-20260721.105615-1.zip")); + } + + @Test + void isSnapshotFlags() { + assertTrue(SnapshotVersions.isSnapshot("0.4.0-SNAPSHOT")); + assertTrue(SnapshotVersions.isSnapshot("0.4.0-20260721.105615-1")); + assertTrue(SnapshotVersions.isUniqueSnapshot("0.4.0-20260721.105615-1")); + assertFalse(SnapshotVersions.isUniqueSnapshot("0.4.0-SNAPSHOT")); + assertFalse(SnapshotVersions.isSnapshot("1.0.0")); + } + + @Test + void versionFolderFromPath() { + assertEquals( + "0.4.0-SNAPSHOT", + SnapshotVersions.versionFolderFromPath( + "/org/apache/hop/hop-datavault/0.4.0-SNAPSHOT/hop-datavault-0.4.0-20260721.105615-1.zip")); + } +} diff --git a/plugins/misc/pom.xml b/plugins/misc/pom.xml index 8c6e6053286..8af8f52b7ae 100644 --- a/plugins/misc/pom.xml +++ b/plugins/misc/pom.xml @@ -35,6 +35,7 @@ git import mail + marketplace passwords projects py4j diff --git a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironment.java b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironment.java index bb4b68e5c03..6c0eba8af87 100644 --- a/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironment.java +++ b/plugins/misc/projects/src/main/java/org/apache/hop/projects/environment/LifecycleEnvironment.java @@ -18,11 +18,20 @@ package org.apache.hop.projects.environment; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import org.apache.hop.core.IAttributes; -/** A project lifecycle environment describes the state of a project and its configuration. */ -public class LifecycleEnvironment { +/** + * A project lifecycle environment describes the state of a project and its configuration. + * + *

Implements {@link IAttributes} so optional plugins (marketplace, resource checks, …) can store + * namespaced group/key/value settings without hard-wiring fields into this class. Groups are + * persisted under {@code attributesMap} in hop-config projects configuration. + */ +public class LifecycleEnvironment implements IAttributes { private String name; @@ -34,8 +43,12 @@ public class LifecycleEnvironment { private List configurationFiles; + /** Group → (key → value); see {@link IAttributes}. */ + private Map> attributesMap; + public LifecycleEnvironment() { configurationFiles = new ArrayList<>(); + attributesMap = new HashMap<>(); } public LifecycleEnvironment( @@ -43,7 +56,8 @@ public LifecycleEnvironment( this.name = name; this.purpose = purpose; this.projectName = projectName; - this.configurationFiles = configurationFiles; + this.configurationFiles = configurationFiles != null ? configurationFiles : new ArrayList<>(); + this.attributesMap = new HashMap<>(); } public LifecycleEnvironment(LifecycleEnvironment env) { @@ -52,6 +66,24 @@ public LifecycleEnvironment(LifecycleEnvironment env) { this.projectName = env.projectName; this.canvasText = env.canvasText; this.configurationFiles = new ArrayList<>(env.configurationFiles); + this.attributesMap = deepCopyAttributes(env.attributesMap); + } + + private static Map> deepCopyAttributes( + Map> source) { + Map> copy = new HashMap<>(); + if (source == null) { + return copy; + } + for (Map.Entry> entry : source.entrySet()) { + if (entry.getKey() == null) { + continue; + } + copy.put( + entry.getKey(), + entry.getValue() == null ? new HashMap<>() : new HashMap<>(entry.getValue())); + } + return copy; } @Override @@ -150,4 +182,52 @@ public List getConfigurationFiles() { public void setConfigurationFiles(List configurationFiles) { this.configurationFiles = configurationFiles; } + + @Override + public void setAttributesMap(Map> attributesMap) { + this.attributesMap = attributesMap != null ? attributesMap : new HashMap<>(); + } + + @Override + public Map> getAttributesMap() { + if (attributesMap == null) { + attributesMap = new HashMap<>(); + } + return attributesMap; + } + + @Override + public void setAttributes(String groupName, Map attributes) { + if (groupName == null) { + return; + } + getAttributesMap().put(groupName, attributes != null ? attributes : new HashMap<>()); + } + + @Override + public void setAttribute(String groupName, String key, String value) { + if (groupName == null || key == null) { + return; + } + Map attributes = getAttributes(groupName); + if (attributes == null) { + attributes = new HashMap<>(); + getAttributesMap().put(groupName, attributes); + } + attributes.put(key, value); + } + + @Override + public Map getAttributes(String groupName) { + return getAttributesMap().get(groupName); + } + + @Override + public String getAttribute(String groupName, String key) { + Map attributes = getAttributes(groupName); + if (attributes == null) { + return null; + } + return attributes.get(key); + } } 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 d70f99078d5..7b7df2a3cdb 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 @@ -22,9 +22,13 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.commons.vfs2.FileObject; +import org.apache.hop.core.AttributesContext; import org.apache.hop.core.Const; import org.apache.hop.core.config.DescribedVariablesConfigFile; import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.extension.ExtensionPointHandler; +import org.apache.hop.core.extension.HopExtensionPoint; +import org.apache.hop.core.logging.LogChannel; import org.apache.hop.core.variables.DescribedVariable; import org.apache.hop.core.variables.IVariables; import org.apache.hop.core.vfs.HopVfs; @@ -34,6 +38,7 @@ import org.apache.hop.projects.project.ProjectConfig; import org.apache.hop.ui.core.ConstUi; import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.AttributesDialogExtension; import org.apache.hop.ui.core.dialog.BaseDialog; import org.apache.hop.ui.core.dialog.EnterSelectionDialog; import org.apache.hop.ui.core.dialog.ErrorDialog; @@ -46,12 +51,14 @@ import org.apache.hop.ui.hopgui.HopGui; import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; import org.eclipse.swt.SWT; +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; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; -import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Dialog; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; @@ -84,6 +91,8 @@ public class LifecycleEnvironmentDialog extends Dialog { private boolean needingEnvironmentRefresh; + private AttributesDialogExtension dialogExtension; + /** Last name we auto-suggested (for detecting when the user edits away from it). */ private String lastSuggestedName; @@ -127,7 +136,6 @@ public String open() { PropsUi.setLook(shell); int margin = PropsUi.getMargin() + 2; - int middle = props.getMiddlePct(); FormLayout formLayout = new FormLayout(); formLayout.marginWidth = PropsUi.getFormMargin(); @@ -146,7 +154,66 @@ public String open() { wCancel.addListener(SWT.Selection, event -> cancel()); BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOK, wCancel}, margin * 3, null); - Label wlName = new Label(shell, SWT.RIGHT); + 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); + + createGeneralTab(wTabFolder, margin); + createConfigurationFilesTab(wTabFolder, margin); + + // Optional plugins (marketplace, resource checks, …) contribute extra tabs + AttributesContext attributesContext = new AttributesContext(environment); + attributesContext.setProjectName(environment.getProjectName()); + attributesContext.setEnvironmentName(environment.getName()); + attributesContext.setPurpose(environment.getPurpose()); + if (environment.getConfigurationFiles() != null) { + attributesContext.setConfigurationFiles(new ArrayList<>(environment.getConfigurationFiles())); + } + dialogExtension = + new AttributesDialogExtension(shell, wTabFolder, variables, attributesContext); + try { + ExtensionPointHandler.callExtensionPoint( + LogChannel.UI, + variables, + HopExtensionPoint.HopGuiLifecycleEnvironmentDialogTabs.id, + dialogExtension); + } catch (Exception e) { + new ErrorDialog(shell, CONST_ERROR, "Error loading optional environment dialog tabs", e); + } + + wTabFolder.setSelection(0); + + getData(); + if (dialogExtension != null) { + dialogExtension.runLoadCallbacks(); + } + + wName.setFocus(); + + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + + return returnValue; + } + + private void createGeneralTab(CTabFolder folder, int margin) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Tab.General")); + 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); + + int middle = props.getMiddlePct(); + + Label wlName = new Label(comp, SWT.RIGHT); PropsUi.setLook(wlName); wlName.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Label.EnvironmentName")); FormData fdlName = new FormData(); @@ -154,7 +221,7 @@ public String open() { fdlName.right = new FormAttachment(middle, 0); fdlName.top = new FormAttachment(0, margin); wlName.setLayoutData(fdlName); - wName = new Text(shell, SWT.SINGLE | SWT.BORDER | SWT.LEFT); + wName = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT); PropsUi.setLook(wName); FormData fdName = new FormData(); fdName.left = new FormAttachment(middle, margin); @@ -162,18 +229,17 @@ public String open() { fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); wName.setLayoutData(fdName); wName.addListener(SWT.Modify, e -> onNameModified()); - Control lastControl = wName; - Label wlPurpose = new Label(shell, SWT.RIGHT); + Label wlPurpose = new Label(comp, SWT.RIGHT); PropsUi.setLook(wlPurpose); wlPurpose.setText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Label.EnvironmentPurpose")); FormData fdlPurpose = new FormData(); fdlPurpose.left = new FormAttachment(0, 0); fdlPurpose.right = new FormAttachment(middle, 0); - fdlPurpose.top = new FormAttachment(lastControl, margin); + fdlPurpose.top = new FormAttachment(wName, margin); wlPurpose.setLayoutData(fdlPurpose); - wPurpose = new Combo(shell, SWT.SINGLE | SWT.BORDER | SWT.LEFT); + wPurpose = new Combo(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT); PropsUi.setLook(wPurpose); FormData fdPurpose = new FormData(); fdPurpose.left = new FormAttachment(middle, margin); @@ -186,18 +252,17 @@ public String open() { needingEnvironmentRefresh = true; updateSuggestedName(); }); - lastControl = wPurpose; - Label wlProject = new Label(shell, SWT.RIGHT); + Label wlProject = new Label(comp, SWT.RIGHT); PropsUi.setLook(wlProject); wlProject.setText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Label.ReferencedProject")); FormData fdlProject = new FormData(); fdlProject.left = new FormAttachment(0, 0); fdlProject.right = new FormAttachment(middle, 0); - fdlProject.top = new FormAttachment(lastControl, margin); + fdlProject.top = new FormAttachment(wPurpose, margin); wlProject.setLayoutData(fdlProject); - wProject = new Combo(shell, SWT.SINGLE | SWT.BORDER | SWT.LEFT); + wProject = new Combo(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT); PropsUi.setLook(wProject); FormData fdProject = new FormData(); fdProject.left = new FormAttachment(middle, margin); @@ -210,18 +275,17 @@ public String open() { needingEnvironmentRefresh = true; updateSuggestedName(); }); - lastControl = wProject; - Label wlCanvasText = new Label(shell, SWT.RIGHT); + Label wlCanvasText = new Label(comp, SWT.RIGHT); PropsUi.setLook(wlCanvasText); wlCanvasText.setText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Label.CanvasText")); FormData fdlCanvasText = new FormData(); fdlCanvasText.left = new FormAttachment(0, 0); fdlCanvasText.right = new FormAttachment(middle, 0); - fdlCanvasText.top = new FormAttachment(lastControl, margin); + fdlCanvasText.top = new FormAttachment(wProject, margin); wlCanvasText.setLayoutData(fdlCanvasText); - wCanvasText = new Text(shell, SWT.SINGLE | SWT.BORDER | SWT.LEFT); + wCanvasText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.LEFT); PropsUi.setLook(wCanvasText); wCanvasText.setToolTipText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.ToolTip.CanvasText")); @@ -230,21 +294,30 @@ public String open() { fdCanvasText.right = new FormAttachment(100, 0); fdCanvasText.top = new FormAttachment(wlCanvasText, 0, SWT.CENTER); wCanvasText.setLayoutData(fdCanvasText); - lastControl = wCanvasText; + } - Label wlConfigFiles = new Label(shell, SWT.LEFT); + private void createConfigurationFilesTab(CTabFolder folder, int margin) { + CTabItem tab = new CTabItem(folder, SWT.NONE); + tab.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Tab.ConfigurationFiles")); + 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); + + Label wlConfigFiles = new Label(comp, SWT.LEFT); PropsUi.setLook(wlConfigFiles); wlConfigFiles.setText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Group.Label.ConfigurationFiles")); FormData fdlConfigFiles = new FormData(); fdlConfigFiles.left = new FormAttachment(0, 0); fdlConfigFiles.right = new FormAttachment(100, 0); - fdlConfigFiles.top = new FormAttachment(lastControl, margin); + fdlConfigFiles.top = new FormAttachment(0, 0); wlConfigFiles.setLayoutData(fdlConfigFiles); - // Bottons to the right. - // - wbImportVariables = new Button(shell, SWT.PUSH); + wbImportVariables = new Button(comp, SWT.PUSH); PropsUi.setLook(wbImportVariables); wbImportVariables.setText( BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.ImportVariables")); @@ -254,7 +327,7 @@ public String open() { wbImportVariables.setLayoutData(fdImportVariables); wbImportVariables.addListener(SWT.Selection, this::importVariables); - Button wbSelect = new Button(shell, SWT.PUSH); + Button wbSelect = new Button(comp, SWT.PUSH); PropsUi.setLook(wbSelect); wbSelect.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.Select")); FormData fdAdd = new FormData(); @@ -264,26 +337,6 @@ public String open() { wbSelect.setLayoutData(fdAdd); wbSelect.addListener(SWT.Selection, this::addConfigFile); - Button wbNew = new Button(shell, SWT.PUSH); - PropsUi.setLook(wbNew); - wbNew.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.New")); - FormData fdNew = new FormData(); - fdNew.left = new FormAttachment(wbImportVariables, 0, SWT.LEFT); - fdNew.right = new FormAttachment(100, 0); - fdNew.top = new FormAttachment(wbSelect, margin); - wbNew.setLayoutData(fdNew); - wbNew.addListener(SWT.Selection, this::newConfigFile); - - wbEdit = new Button(shell, SWT.PUSH); - PropsUi.setLook(wbEdit); - wbEdit.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.Edit")); - FormData fdEdit = new FormData(); - fdEdit.left = new FormAttachment(wbImportVariables, 0, SWT.LEFT); - fdEdit.right = new FormAttachment(100, 0); - fdEdit.top = new FormAttachment(wbNew, margin); - wbEdit.setLayoutData(fdEdit); - wbEdit.addListener(SWT.Selection, this::editConfigFile); - ColumnInfo[] columnInfo = new ColumnInfo[] { new ColumnInfo( @@ -297,10 +350,10 @@ public String open() { wConfigFiles = new TableView( variables, - shell, + comp, SWT.SINGLE | SWT.BORDER, columnInfo, - environment.getConfigurationFiles().size(), + Math.max(environment.getConfigurationFiles().size(), 1), null, props); PropsUi.setLook(wConfigFiles); @@ -308,17 +361,29 @@ public String open() { fdConfigFiles.left = new FormAttachment(0, 0); fdConfigFiles.right = new FormAttachment(wbImportVariables, -2 * margin); fdConfigFiles.top = new FormAttachment(wlConfigFiles, margin); - fdConfigFiles.bottom = new FormAttachment(wOK, -margin * 2); + fdConfigFiles.bottom = new FormAttachment(100, 0); wConfigFiles.setLayoutData(fdConfigFiles); wConfigFiles.table.addListener(SWT.Selection, this::setButtonStates); - getData(); - - wName.setFocus(); - - BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + Button wbNew = new Button(comp, SWT.PUSH); + PropsUi.setLook(wbNew); + wbNew.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.New")); + FormData fdNew = new FormData(); + fdNew.left = new FormAttachment(wConfigFiles, 2 * margin); + fdNew.right = new FormAttachment(100, 0); + fdNew.top = new FormAttachment(wbSelect, margin); + wbNew.setLayoutData(fdNew); + wbNew.addListener(SWT.Selection, this::newConfigFile); - return returnValue; + wbEdit = new Button(comp, SWT.PUSH); + PropsUi.setLook(wbEdit); + wbEdit.setText(BaseMessages.getString(PKG, "LifecycleEnvironmentDialog.Button.Edit")); + FormData fdEdit = new FormData(); + fdEdit.left = new FormAttachment(wConfigFiles, 2 * margin); + fdEdit.right = new FormAttachment(100, 0); + fdEdit.top = new FormAttachment(wbNew, margin); + wbEdit.setLayoutData(fdEdit); + wbEdit.addListener(SWT.Selection, this::editConfigFile); } private void editConfigFile(Event event) { @@ -437,10 +502,6 @@ private void setButtonStates(Event event) { wbEdit.setGrayed(index < 0); } - /** - * Crawl the project for {@code ${VARIABLE}} expressions not yet defined in environment config - * files, review them, and write into a new or existing configuration file. - */ private void importVariables(Event event) { shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT)); try { @@ -634,6 +695,16 @@ private void ok() { } getInfo(environment); + if (dialogExtension != null) { + dialogExtension.getContext().setProjectName(environment.getProjectName()); + dialogExtension.getContext().setEnvironmentName(environment.getName()); + dialogExtension.getContext().setPurpose(environment.getPurpose()); + dialogExtension + .getContext() + .setConfigurationFiles(new ArrayList<>(environment.getConfigurationFiles())); + dialogExtension.runSaveCallbacks(); + dialogExtension.getContext().copyAttributesTo(environment); + } returnValue = environment.getName(); dispose(); @@ -705,11 +776,15 @@ private void getData() { setNameText(""); } + wConfigFiles.table.removeAll(); for (int i = 0; i < environment.getConfigurationFiles().size(); i++) { String configurationFile = environment.getConfigurationFiles().get(i); - TableItem item = wConfigFiles.table.getItem(i); + TableItem item = new TableItem(wConfigFiles.table, SWT.NONE); item.setText(1, Const.NVL(configurationFile, "")); } + if (environment.getConfigurationFiles().isEmpty()) { + new TableItem(wConfigFiles.table, SWT.NONE); + } wConfigFiles.setRowNums(); wConfigFiles.optWidth(true); 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 ac30f082a83..64b24d775c4 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 @@ -23,11 +23,13 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileSystemException; +import org.apache.hop.core.AttributesContext; import org.apache.hop.core.Const; import org.apache.hop.core.DbCache; import org.apache.hop.core.encryption.Encr; import org.apache.hop.core.exception.HopException; import org.apache.hop.core.extension.ExtensionPointHandler; +import org.apache.hop.core.extension.HopExtensionPoint; import org.apache.hop.core.logging.ILogChannel; import org.apache.hop.core.variables.IVariables; import org.apache.hop.core.vfs.HopVfs; @@ -38,6 +40,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.Project; import org.apache.hop.projects.project.ProjectConfig; import org.apache.hop.ui.core.gui.HopNamespace; @@ -147,6 +150,57 @@ public static void enableProject( // ExtensionPointHandler.callExtensionPoint( log, variables, Defaults.EXTENSION_POINT_PROJECT_ACTIVATED, projectName); + + // Plugin-agnostic attributes context for marketplace, resource checks, etc. + // Thrown HopException from listeners aborts environment enablement. + // + AttributesContext attributesContext = + buildAttributesContext(config, projectConfig, projectName, environmentName, variables); + ExtensionPointHandler.callExtensionPoint( + log, variables, HopExtensionPoint.HopProjectEnvironmentAfterEnabled.id, attributesContext); + } + + /** + * Build a core {@link AttributesContext} for the enabled project/environment so optional plugins + * can read identity fields and namespaced {@link org.apache.hop.core.IAttributes} groups without + * depending on Projects classes. + */ + public static AttributesContext buildAttributesContext( + ProjectsConfig config, + ProjectConfig projectConfig, + String projectName, + String environmentName, + IVariables variables) + throws HopException { + AttributesContext context = new AttributesContext(); + context.setProjectName(projectName); + context.setEnvironmentName(environmentName); + + if (projectConfig != null) { + try { + String home = projectConfig.getProjectHome(); + if (variables != null && StringUtils.isNotEmpty(home)) { + home = variables.resolve(home); + } + context.setProjectHome(home); + } catch (Exception e) { + // best-effort project home + context.setProjectHome(projectConfig.getProjectHome()); + } + } + + LifecycleEnvironment environment = + StringUtils.isNotEmpty(environmentName) && config != null + ? config.findEnvironment(environmentName) + : null; + if (environment != null) { + context.setPurpose(environment.getPurpose()); + if (environment.getConfigurationFiles() != null) { + context.setConfigurationFiles(new ArrayList<>(environment.getConfigurationFiles())); + } + context.copyAttributesFrom(environment); + } + return context; } public static void validateFileInProject( diff --git a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties index a4ee2c7c00a..041a367a3fd 100644 --- a/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties +++ b/plugins/misc/projects/src/main/resources/org/apache/hop/projects/environment/messages/messages_en_US.properties @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# LifecycleEnvironmentDialog.Button.Edit=Edit... LifecycleEnvironmentDialog.Button.ImportVariables=Import variables @@ -34,6 +33,8 @@ LifecycleEnvironmentDialog.Purpose.Text.Development=Development LifecycleEnvironmentDialog.Purpose.Text.Production=Production LifecycleEnvironmentDialog.Purpose.Text.Testing=Testing LifecycleEnvironmentDialog.Shell.Name=Environment Properties +LifecycleEnvironmentDialog.Tab.General=General +LifecycleEnvironmentDialog.Tab.ConfigurationFiles=Configuration files LifecycleEnvironmentDialog.ImportVariables.ProjectRequired.Title=Project required LifecycleEnvironmentDialog.ImportVariables.ProjectRequired.Message=Please select the project associated with this environment before importing variables. LifecycleEnvironmentDialog.ImportVariables.NoneFound.Title=No variables to import @@ -41,7 +42,7 @@ LifecycleEnvironmentDialog.ImportVariables.NoneFound.Message=No '${VARIABLE}' ex LifecycleEnvironmentDialog.ImportVariables.DialogMessage=Variables used in project ''{0}'' that are not yet in an environment configuration file. Set values as needed, then OK to save. LifecycleEnvironmentDialog.ImportVariables.SelectConfig.Title=Select configuration file LifecycleEnvironmentDialog.ImportVariables.SelectConfig.Message=Choose an existing environment configuration file, or create a new one. -LifecycleEnvironmentDialog.ImportVariables.CreateNewOption=(Create new configuration file\u2026) +LifecycleEnvironmentDialog.ImportVariables.CreateNewOption=(Create new configuration file…) LifecycleEnvironmentDialog.ImportVariables.FileFilter.Json=Config JSON files LifecycleEnvironmentDialog.ImportVariables.FileFilter.All=All files LifecycleEnvironmentDialog.ImportVariables.Saved.Title=Variables saved diff --git a/pom.xml b/pom.xml index f6e0f5a877c..c69086e4656 100644 --- a/pom.xml +++ b/pom.xml @@ -323,7 +323,13 @@ maven-release-plugin - -P=-assemblies,-assembly -DskipTests + + -P=-assemblies -DskipTests @@ -898,6 +904,11 @@ deploy-snapshots @@ -908,15 +919,8 @@ ${project.basedir}/local-snapshots-dir ** - **/hop-assemblies*/** - hop-plugins/**.zip - hop-core/**.zip - hop-engine/**.zip - hop-ui-rap/**.zip - hop-ui-rcp/**.zip - hop-rest/**.zip - hop-ui/**.zip - **/org.eclipse.tm4e.core/** + + **/hop-assemblies*/**,hop-core/**/*.zip,hop-engine/**/*.zip,hop-ui/**/*.zip,hop-ui-rap/**/*.zip,hop-ui-rcp/**/*.zip,hop-rest/**/*.zip,**/org.eclipse.tm4e.core/** apache.snapshots.https ${distMgmtSnapshotsUrl} diff --git a/tools/build-marketplace-list.sh b/tools/build-marketplace-list.sh new file mode 100755 index 00000000000..80c324c58c7 --- /dev/null +++ b/tools/build-marketplace-list.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# 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 +# +# Compatibility wrapper: list marketplace plugin zip paths from optional-plugins.yaml. +# Prefer: ./tools/list-marketplace-plugins.sh --zips +# +# Output lines: artifactId|modulePath/target/artifactId-${HOP_VERSION}.zip +# +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +exec "${ROOT}/tools/list-marketplace-plugins.sh" --zips diff --git a/tools/check-assembly-size.sh b/tools/check-assembly-size.sh new file mode 100755 index 00000000000..b6072a4e9ba --- /dev/null +++ b/tools/check-assembly-size.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# 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. +# +# Fail if published mega-artifacts exceed the ASF Artifactory package limit. +set -euo pipefail + +# 850 MiB ASF limit; use a safer default threshold of 800 MiB +MAX_BYTES=${MAX_ASSEMBLY_BYTES:-838860800} +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +FAILED=0 + +check() { + local label="$1" + local file="$2" + if [[ ! -f "$file" ]]; then + echo "SKIP (missing): $label — $file" + return 0 + fi + local size + size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file") + local mb + mb=$(awk -v s="$size" 'BEGIN { printf "%.1f", s/1024/1024 }') + if (( size > MAX_BYTES )); then + echo "FAIL: $label is ${mb} MB ($(printf '%s' "$size") bytes) > limit $(awk -v s="$MAX_BYTES" 'BEGIN { printf "%.0f", s/1024/1024 }') MB" + FAILED=1 + else + echo "OK: $label is ${mb} MB" + fi +} + +shopt -s nullglob +for f in "$ROOT"/assemblies/client/target/hop-client-*.zip; do + check "hop-client" "$f" +done +for f in "$ROOT"/assemblies/plugins/target/hop-assemblies-plugins-*.zip; do + # plugins aggregate should also stay under the limit + check "hop-assemblies-plugins" "$f" +done + +if (( FAILED )); then + echo "Assembly size check failed (limit ${MAX_BYTES} bytes)." + exit 1 +fi +echo "Assembly size check passed." diff --git a/tools/deploy-snapshots-data-hopper.sh b/tools/deploy-snapshots-data-hopper.sh new file mode 100755 index 00000000000..71d3af16992 --- /dev/null +++ b/tools/deploy-snapshots-data-hopper.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# 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 +# +# Deploy Hop SNAPSHOT artifacts to a private Maven repo (e.g. data-hopper), +# similar in spirit to Jenkins local deploy + upload — without changing the +# committed apache/hop distributionManagement. +# +# Modes: +# marketplace (default) — package + deploy-file all optional-plugins.yaml zips +# reactor — full mvn clean deploy (-P=-assemblies) to the repo +# +# Credentials (never commit these): +# marketplace mode: HOP_DEPLOY_USER + HOP_DEPLOY_PASSWORD (or NEXUS_* / ARTIFACTORY_*) +# reactor mode: ~/.m2/settings.xml server id matching DATA_HOPPER_SERVER_ID +# +# Usage (from Hop repo root): +# export HOP_DEPLOY_PASSWORD='…' +# ./tools/deploy-snapshots-data-hopper.sh +# ./tools/deploy-snapshots-data-hopper.sh marketplace +# ./tools/deploy-snapshots-data-hopper.sh reactor +# +# Env overrides: +# DATA_HOPPER_URL default https://repository.data-hopper.com/repository/apache-hop-plugins/ +# DATA_HOPPER_SERVER_ID default apache-hop-plugins +# HOP_DEPLOY_USER default hop_build +# HOP_DEPLOY_PASSWORD required for marketplace mode +# HOP_VERSION default 2.19.0-SNAPSHOT +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +URL="${DATA_HOPPER_URL:-https://repository.data-hopper.com/repository/apache-hop-plugins/}" +URL="${URL%/}" +ID="${DATA_HOPPER_SERVER_ID:-apache-hop-plugins}" +MODE="${1:-marketplace}" +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" +USER="${HOP_DEPLOY_USER:-${NEXUS_USER:-${ARTIFACTORY_USER:-hop_build}}}" +PASS="${HOP_DEPLOY_PASSWORD:-${NEXUS_PASSWORD:-${ARTIFACTORY_PASSWORD:-}}}" + +MVN="${ROOT}/mvnw" +if [[ ! -x "${MVN}" ]]; then + MVN=mvn +fi + +case "${MODE}" in + marketplace | plugins) + if [[ -z "${PASS}" ]]; then + echo "Set HOP_DEPLOY_PASSWORD (or NEXUS_PASSWORD / ARTIFACTORY_PASSWORD) for marketplace deploy." >&2 + exit 1 + fi + echo "==> Marketplace plugin SNAPSHOTs → ${URL}" + echo " server id: ${ID} user: ${USER} version: ${VERSION}" + # Force data-hopper (or override) — must win over docker/marketplace-nexus/.env + export ARTIFACTORY_URL="${URL}" + export NEXUS_REPO_URL="${URL}" + export NEXUS_URL="${URL}" + export NEXUS_REPO_ID="${ID}" + export ARTIFACTORY_USER="${USER}" + export NEXUS_USER="${USER}" + # Do not let local .env admin password replace hop_build + export ARTIFACTORY_PASSWORD="${PASS}" + export NEXUS_PASSWORD="${PASS}" + unset NEXUS_ADMIN_PASSWORD 2>/dev/null || true + export HOP_VERSION="${VERSION}" + exec "${ROOT}/docker/marketplace-nexus/publish-marketplace-plugins.sh" --package + ;; + + reactor | full) + echo "==> Reactor SNAPSHOT deploy (-P=-assemblies) → ${URL}" + echo " server id: ${ID} (credentials from ~/.m2/settings.xml)" + echo " Requires ${ID}… in settings.xml" + # shellcheck disable=SC2086 + exec "${MVN}" -f "${ROOT}/pom.xml" clean deploy \ + -DskipTests \ + -P=-assemblies \ + -DaltDeploymentRepository="${ID}::default::${URL}/" \ + ${MVN_EXTRA_ARGS:-} + ;; + + -h | --help | help) + sed -n '2,45p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + + *) + echo "Usage: $0 [marketplace|reactor]" >&2 + exit 2 + ;; +esac diff --git a/tools/generate-full-client-env.sh b/tools/generate-full-client-env.sh new file mode 100755 index 00000000000..7190f3e73fa --- /dev/null +++ b/tools/generate-full-client-env.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# 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 +# +# Generate full-client-env.yaml from optional-plugins.yaml (marketplace registry). +# +# Usage: +# ./tools/generate-full-client-env.sh [HOP_VERSION] [OUTPUT_PATH] +# +# Defaults: +# HOP_VERSION from ${project.version} when set by Maven, else 2.19.0-SNAPSHOT +# OUTPUT_PATH plugins/misc/marketplace/target/generated/full-client-env.yaml +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REGISTRY="${ROOT}/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml" +VERSION="${1:-${HOP_VERSION:-2.19.0-SNAPSHOT}}" +OUT="${2:-${ROOT}/plugins/misc/marketplace/target/generated/full-client-env.yaml}" + +if [[ ! -f "${REGISTRY}" ]]; then + echo "Registry not found: ${REGISTRY}" >&2 + exit 1 +fi + +mkdir -p "$(dirname "${OUT}")" + +# Extract artifactIds (lines like " - artifactId: hop-tech-parquet") +mapfile -t ARTIFACTS < <(sed -n 's/^[[:space:]]*- artifactId:[[:space:]]*\(.*\)$/\1/p' "${REGISTRY}") + +if [[ ${#ARTIFACTS[@]} -eq 0 ]]; then + echo "No plugins found in ${REGISTRY}" >&2 + exit 1 +fi + +{ + cat <"${OUT}" + +echo "Wrote ${OUT} (${#ARTIFACTS[@]} plugins, hopVersion=${VERSION})" diff --git a/tools/install-wave1-plugins.sh b/tools/install-wave1-plugins.sh new file mode 100755 index 00000000000..7a81cacc554 --- /dev/null +++ b/tools/install-wave1-plugins.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# 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 +# +# Unpack marketplace-optional plugin zips into a Hop install (e.g. for IT images). +# Plugin list is read only from optional-plugins.yaml (via list-marketplace-plugins.sh). +# +# Usage: +# ./tools/install-wave1-plugins.sh [HOP_INSTALL_DIR] +# Default install dir: assemblies/client/target/hop +# +# Requires plugin modules to have been packaged (*.zip under plugins/**/target/). +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTALL_DIR="${1:-${ROOT}/assemblies/client/target/hop}" +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" +LIST_SCRIPT="${ROOT}/tools/list-marketplace-plugins.sh" + +if [[ ! -d "${INSTALL_DIR}" ]]; then + echo "Hop install not found: ${INSTALL_DIR}" >&2 + echo "Unzip hop-client first, or pass an existing install path." >&2 + exit 1 +fi + +if [[ ! -x "${LIST_SCRIPT}" ]]; then + chmod +x "${LIST_SCRIPT}" 2>/dev/null || true +fi + +mapfile -t PLUGINS < <(HOP_VERSION="${VERSION}" "${LIST_SCRIPT}" --zips) + +installed=0 +skipped=0 +for entry in "${PLUGINS[@]}"; do + id="${entry%%|*}" + rel="${entry#*|}" + zip="${ROOT}/${rel}" + if [[ ! -f "${zip}" ]]; then + echo "SKIP (not built): ${id} — ${rel}" + skipped=$((skipped + 1)) + continue + fi + echo "Installing ${id} from ${rel}" + unzip -o -q "${zip}" -d "${INSTALL_DIR}" + installed=$((installed + 1)) +done + +echo "Marketplace plugins: installed=${installed} skipped=${skipped} into ${INSTALL_DIR}" +# Note: beam plugin zip includes lib-beam (Beam SDKs). diff --git a/tools/list-marketplace-plugins.sh b/tools/list-marketplace-plugins.sh new file mode 100755 index 00000000000..8a1b55ec568 --- /dev/null +++ b/tools/list-marketplace-plugins.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# 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 +# +# List marketplace-optional plugins from optional-plugins.yaml (source of truth). +# +# Output (one line per unique artifactId, registry order): +# artifactId|modulePath +# +# Usage: +# ./tools/list-marketplace-plugins.sh +# HOP_VERSION=2.19.0-SNAPSHOT ./tools/list-marketplace-plugins.sh --zips +# → artifactId|modulePath/target/artifactId-${HOP_VERSION}.zip +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +REGISTRY="${ROOT}/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml" +VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}" +MODE="pairs" + +if [[ "${1:-}" == "--zips" ]]; then + MODE="zips" +elif [[ "${1:-}" == "--modules" ]]; then + MODE="modules" +elif [[ -n "${1:-}" ]]; then + echo "Usage: $0 [--zips|--modules]" >&2 + exit 2 +fi + +if [[ ! -f "${REGISTRY}" ]]; then + echo "Registry not found: ${REGISTRY}" >&2 + exit 1 +fi + +# Parse simple YAML entries: - artifactId: … then modulePath: … +# Deduplicate by artifactId (first wins). No yq required. +awk ' + /^[[:space:]]*-[[:space:]]*artifactId:[[:space:]]*/ { + if (aid != "" && path != "" && !(aid in seen)) { + seen[aid] = 1 + print aid "|" path + order[++n] = aid + } + line = $0 + sub(/^[[:space:]]*-[[:space:]]*artifactId:[[:space:]]*/, "", line) + gsub(/[[:space:]]+$/, "", line) + gsub(/^["'\'']|["'\'']$/, "", line) + aid = line + path = "" + next + } + /^[[:space:]]*modulePath:[[:space:]]*/ { + line = $0 + sub(/^[[:space:]]*modulePath:[[:space:]]*/, "", line) + gsub(/[[:space:]]+$/, "", line) + gsub(/^["'\'']|["'\'']$/, "", line) + path = line + next + } + END { + if (aid != "" && path != "" && !(aid in seen)) { + print aid "|" path + } + } +' "${REGISTRY}" | while IFS='|' read -r artifactId modulePath; do + [[ -n "${artifactId}" && -n "${modulePath}" ]] || continue + case "${MODE}" in + pairs) + printf '%s|%s\n' "${artifactId}" "${modulePath}" + ;; + zips) + printf '%s|%s/target/%s-%s.zip\n' \ + "${artifactId}" "${modulePath}" "${artifactId}" "${VERSION}" + ;; + modules) + printf '%s\n' "${modulePath}" + ;; + esac +done diff --git a/tools/verify-ci-snapshot-zips.sh b/tools/verify-ci-snapshot-zips.sh new file mode 100755 index 00000000000..102c320fa9b --- /dev/null +++ b/tools/verify-ci-snapshot-zips.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# 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 +# +# After a Jenkins-like: +# mvn … -DaltDeploymentRepository=…::file:./local-snapshots-dir clean deploy +# verify that marketplace-optional plugins have zip artifacts in that tree +# (what wagon:upload would send to ASF snapshots, given current deploy-snapshots +# excludes that keep plugin zips). +# +# Usage (repo root): +# ./tools/verify-ci-snapshot-zips.sh +# ./tools/verify-ci-snapshot-zips.sh ./local-snapshots-dir +# +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIR="${1:-${ROOT}/local-snapshots-dir}" +LIST="${ROOT}/tools/list-marketplace-plugins.sh" +GROUP_PATH="org/apache/hop" + +if [[ ! -d "${DIR}" ]]; then + echo "ERROR: directory not found: ${DIR}" >&2 + echo "Run a Jenkins-like clean deploy first, e.g.:" >&2 + echo " rm -rf local-snapshots-dir && mkdir local-snapshots-dir" >&2 + echo " ./mvnw -T 2 -B -DskipTests \\" >&2 + echo " -DaltDeploymentRepository=snapshot-repo::default::file:\$(pwd)/local-snapshots-dir \\" >&2 + echo " clean deploy" >&2 + exit 1 +fi + +if [[ ! -x "${LIST}" ]]; then + chmod +x "${LIST}" 2>/dev/null || true +fi + +mapfile -t IDS < <("${LIST}" | cut -d'|' -f1 | awk 'NF && !seen[$0]++') +if [[ ${#IDS[@]} -eq 0 ]]; then + echo "ERROR: no plugins listed from optional-plugins.yaml" >&2 + exit 1 +fi + +echo "==> Checking marketplace plugin zips under ${DIR}" +echo " (registry: ${#IDS[@]} artifactIds)" +echo + +ok=0 +missing=0 +declare -a MISSING=() + +for id in "${IDS[@]}"; do + # Any zip under the GAV folder (SNAPSHOT unique names included) + hits=$(find "${DIR}/${GROUP_PATH}/${id}" -name '*.zip' 2>/dev/null | wc -l | tr -d ' ') + if [[ "${hits}" -gt 0 ]]; then + sample=$(find "${DIR}/${GROUP_PATH}/${id}" -name '*.zip' 2>/dev/null | head -1) + echo " OK ${id} (${hits} zip(s), e.g. ${sample#${DIR}/})" + ok=$((ok + 1)) + else + echo " MISSING ${id}" + missing=$((missing + 1)) + MISSING+=("${id}") + fi +done + +echo +echo "Summary: ok=${ok} missing=${missing} (of ${#IDS[@]})" + +# Remind about wagon excludes (plugin zips are intentionally kept) +echo +echo "Jenkins Deploy stage uses -P deploy-snapshots wagon:upload from local-snapshots-dir." +echo "Current excludes drop hop-assemblies* and core/engine/ui zips — not marketplace plugins." +echo "After merge to main, confirm a zip on ASF snapshots, e.g.:" +echo " https://repository.apache.org/content/repositories/snapshots/org/apache/hop/hop-tech-parquet/" + +if [[ "${missing}" -gt 0 ]]; then + echo >&2 + echo "FAILED — no zip in ${DIR} for:" >&2 + printf ' - %s\n' "${MISSING[@]}" >&2 + exit 1 +fi + +echo +echo "PASSED — all marketplace-optional plugins have zips in the CI deploy tree." diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/AttributesDialogExtension.java b/ui/src/main/java/org/apache/hop/ui/core/dialog/AttributesDialogExtension.java new file mode 100644 index 00000000000..79c3e6425ef --- /dev/null +++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/AttributesDialogExtension.java @@ -0,0 +1,79 @@ +/* + * 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.ui.core.dialog; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import lombok.Getter; +import org.apache.hop.core.AttributesContext; +import org.apache.hop.core.variables.IVariables; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.widgets.Shell; + +/** + * Payload for dialog-tab contribution extension points (e.g. lifecycle environment dialog). + * + *

Optional plugins receive a {@link CTabFolder} to add tabs and a mutable {@link + * AttributesContext} for namespaced settings. Register load/save callbacks so the host dialog can + * refresh widgets from attributes and flush widget values back before OK. + */ +@Getter +public class AttributesDialogExtension { + + private final Shell shell; + private final CTabFolder tabFolder; + private final IVariables variables; + private final AttributesContext context; + private final List> loadCallbacks = new ArrayList<>(); + private final List> saveCallbacks = new ArrayList<>(); + + public AttributesDialogExtension( + Shell shell, CTabFolder tabFolder, IVariables variables, AttributesContext context) { + this.shell = shell; + this.tabFolder = tabFolder; + this.variables = variables; + this.context = context; + } + + /** Called by the host after building standard tabs so plugins can create widgets. */ + public void addLoadCallback(Consumer callback) { + if (callback != null) { + loadCallbacks.add(callback); + } + } + + /** Called by the host before persisting so plugins can write widgets into attributes. */ + public void addSaveCallback(Consumer callback) { + if (callback != null) { + saveCallbacks.add(callback); + } + } + + public void runLoadCallbacks() { + for (Consumer callback : loadCallbacks) { + callback.accept(context); + } + } + + public void runSaveCallbacks() { + for (Consumer callback : saveCallbacks) { + callback.accept(context); + } + } +}