From d41206c54b7d42df60872e29c5bd4bc30f7af055 Mon Sep 17 00:00:00 2001 From: "Ricardo M." Date: Wed, 22 May 2024 16:22:36 +0200 Subject: [PATCH] feat(catalog): Dynamically build Camel Catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a Camel Catalog generator CLI to build the following structure: ``` dist/ ├─ camel-main/ │ ├─ 4.4.0/ │ ├─ 4.6.0/ ├─ camel-quarkus/ │ ├─ 3.8.0/ ├─ camel-springboot/ │ ├─ 4.4.0/ │ ├─ 4.6.0/ ├─ index.json ``` * How to run? ```bash mvn package; java -jar ./target/catalog-generator-0.0.1-SNAPSHOT.jar -o ./dist -k 4.6.0 -m 4.6.0 -m 4.4.0 -q 3.8.0 -s 4.6.0 ``` This will generate: * Camel Main 4.4.0 * Camel Main 4.6.0 * Camel Quarkus 3.8.0 * Camel SpringBoot 4.6.0 * Usage ``` usage: catalog-generator -k,--kamelets Kamelets catalog version -m,--main Camel Main version -o,--output Output directory. It will be cleaned before generating the catalogs -q,--quarkus Camel Extensions for Quarkus version -s,--springboot Camel SpringBoot version -v,--verbose Be more verbose ``` relates: https://github.com/KaotoIO/kaoto/issues/1109 --- .../kaoto-camel-catalog-maven-plugin/pom.xml | 1 - packages/catalog-generator/.gitignore | 3 + .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 17 + .../catalog-generator/.vscode/settings.json | 4 + packages/catalog-generator/README.md | 17 + packages/catalog-generator/mvnw | 250 +++++++ packages/catalog-generator/mvnw.cmd | 146 ++++ packages/catalog-generator/package.json | 40 ++ packages/catalog-generator/pom.xml | 177 +++++ .../scripts/json-schema-to-typescript.mts | 115 ++++ .../java/io/kaoto/camelcatalog/Build.java | 26 + .../main/java/io/kaoto/camelcatalog/Main.java | 13 + .../kaoto/camelcatalog/beans/ConfigBean.java | 58 ++ .../commands/GenerateCommand.java | 87 +++ .../commands/GenerateCommandOptions.java | 125 ++++ .../generator/CamelCatalogProcessor.java | 635 +++++++++++++++++ .../generator/CamelYamlDSLKeysComparator.java | 27 + .../CamelYamlDslSchemaProcessor.java | 648 ++++++++++++++++++ .../generator/CatalogGeneratorBuilder.java | 440 ++++++++++++ .../generator/K8sSchemaProcessor.java | 132 ++++ .../generator/KameletProcessor.java | 39 ++ .../io/kaoto/camelcatalog/generator/Util.java | 51 ++ .../maven/CamelCatalogVersionLoader.java | 314 +++++++++ .../maven/KaotoMavenVersionManager.java | 179 +++++ .../maven/KaotoOpenURLClassLoader.java | 39 ++ .../model/CatalogCliArgument.java | 60 ++ .../camelcatalog/model/CatalogDefinition.java | 71 ++ .../model/CatalogDefinitionEntry.java | 3 + .../camelcatalog/model/CatalogLibrary.java | 31 + .../model/CatalogLibraryEntry.java | 3 + .../camelcatalog/model/CatalogRuntime.java | 17 + .../kaoto/camelcatalog/model/Constants.java | 39 ++ .../camelcatalog/model/MavenCoordinates.java | 25 + .../kamelet-boundaries/sink.kamelet.yaml | 26 + .../kamelet-boundaries/source.kamelet.yaml | 26 + .../schemas/KameletConfiguration.json | 139 ++++ .../resources/schemas/PipeConfiguration.json | 35 + .../resources/schemas/PipeErrorHandler.json | 111 +++ .../java/io/kaoto/camelcatalog/MainTest.java | 13 + packages/catalog-generator/tsconfig.json | 25 + yarn.lock | 34 +- 42 files changed, 4227 insertions(+), 14 deletions(-) create mode 100644 packages/catalog-generator/.gitignore create mode 100644 packages/catalog-generator/.mvn/wrapper/maven-wrapper.jar create mode 100644 packages/catalog-generator/.mvn/wrapper/maven-wrapper.properties create mode 100644 packages/catalog-generator/.vscode/settings.json create mode 100644 packages/catalog-generator/README.md create mode 100755 packages/catalog-generator/mvnw create mode 100644 packages/catalog-generator/mvnw.cmd create mode 100644 packages/catalog-generator/package.json create mode 100644 packages/catalog-generator/pom.xml create mode 100644 packages/catalog-generator/scripts/json-schema-to-typescript.mts create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Build.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Main.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/beans/ConfigBean.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommand.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommandOptions.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparator.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CatalogGeneratorBuilder.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessor.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/KameletProcessor.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/CamelCatalogVersionLoader.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoMavenVersionManager.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoOpenURLClassLoader.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogCliArgument.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinition.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinitionEntry.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibrary.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibraryEntry.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogRuntime.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/Constants.java create mode 100644 packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/MavenCoordinates.java create mode 100644 packages/catalog-generator/src/main/resources/kamelet-boundaries/sink.kamelet.yaml create mode 100644 packages/catalog-generator/src/main/resources/kamelet-boundaries/source.kamelet.yaml create mode 100644 packages/catalog-generator/src/main/resources/schemas/KameletConfiguration.json create mode 100644 packages/catalog-generator/src/main/resources/schemas/PipeConfiguration.json create mode 100644 packages/catalog-generator/src/main/resources/schemas/PipeErrorHandler.json create mode 100644 packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/MainTest.java create mode 100644 packages/catalog-generator/tsconfig.json diff --git a/packages/camel-catalog/kaoto-camel-catalog-maven-plugin/pom.xml b/packages/camel-catalog/kaoto-camel-catalog-maven-plugin/pom.xml index e0f4a6c67..d33792477 100644 --- a/packages/camel-catalog/kaoto-camel-catalog-maven-plugin/pom.xml +++ b/packages/camel-catalog/kaoto-camel-catalog-maven-plugin/pom.xml @@ -16,7 +16,6 @@ https://kaoto.io - org.apache.maven diff --git a/packages/catalog-generator/.gitignore b/packages/catalog-generator/.gitignore new file mode 100644 index 000000000..6628c4b24 --- /dev/null +++ b/packages/catalog-generator/.gitignore @@ -0,0 +1,3 @@ +target/ +.idea/ +dependency-reduced-pom.xml diff --git a/packages/catalog-generator/.mvn/wrapper/maven-wrapper.jar b/packages/catalog-generator/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/packages/catalog-generator/.mvn/wrapper/maven-wrapper.properties b/packages/catalog-generator/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..f800e78dc --- /dev/null +++ b/packages/catalog-generator/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,17 @@ +# 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/packages/catalog-generator/.vscode/settings.json b/packages/catalog-generator/.vscode/settings.json new file mode 100644 index 000000000..837500fd7 --- /dev/null +++ b/packages/catalog-generator/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx16G -Xms100m -Xlog:disable", + "java.configuration.updateBuildConfiguration": "interactive" +} diff --git a/packages/catalog-generator/README.md b/packages/catalog-generator/README.md new file mode 100644 index 000000000..04a61055e --- /dev/null +++ b/packages/catalog-generator/README.md @@ -0,0 +1,17 @@ +# Kaoto Camel Catalog Generator + +This project is a simple tool to generate a Camel Catalog from a set of Camel components. It is based on the Camel Catalog Maven Plugin and the Camel Catalog Model. + +## Usage + +Install the project dependencies: + +```bash +mvn install +``` + +Run the project with the following command: + +```bash +mvn package; java -jar ./target/catalog-generator-0.0.1-SNAPSHOT.jar -o ./dist -k 4.6.0 -m 4.6.0 -m 4.4.0 -m 4.4.0.redhat-00025 -q 3.8.0 -s 4.6.0 +``` diff --git a/packages/catalog-generator/mvnw b/packages/catalog-generator/mvnw new file mode 100755 index 000000000..08303327c --- /dev/null +++ b/packages/catalog-generator/mvnw @@ -0,0 +1,250 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.0 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl="${value-}" ;; + distributionSha256Sum) distributionSha256Sum="${value-}" ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( new java.net.URL( args[0] ).openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/packages/catalog-generator/mvnw.cmd b/packages/catalog-generator/mvnw.cmd new file mode 100644 index 000000000..33cbf988c --- /dev/null +++ b/packages/catalog-generator/mvnw.cmd @@ -0,0 +1,146 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.0 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/packages/catalog-generator/package.json b/packages/catalog-generator/package.json new file mode 100644 index 000000000..2c81e6a4d --- /dev/null +++ b/packages/catalog-generator/package.json @@ -0,0 +1,40 @@ +{ + "name": "@kaoto/catalog-generator", + "version": "2.1.0-dev", + "type": "commonjs", + "description": "Camel Catalog and schemas for Kaoto", + "repository": "https://github.com/KaotoIO/kaoto", + "repositoryDirectory": "packages/catalog-generator", + "author": "The Kaoto Team", + "license": "Apache License v2.0", + "private": true, + "main": "./package.json", + "exports": { + ".": "./dist", + "./index.json": "./dist/index.json", + "./types": "./dist/types/index.ts", + "./package.json": "./package.json", + "./*.json": "./dist/*.json" + }, + "scripts": { + "build": "yarn clean && yarn build:mvn && yarn build:default:catalog && yarn build:ts", + "build:mvn": "./mvnw clean install", + "build:default:catalog": "./mvnw package; java -jar ./target/catalog-generator-0.0.1-SNAPSHOT.jar -o ./dist -n \"Default Kaoto catalog\"", + "build:ts": "node --loader ts-node/esm ./scripts/json-schema-to-typescript.mts", + "lint": "yarn eslint \"scripts/**/*.{mts,ts}\"", + "lint:fix": "yarn lint --fix", + "clean": "yarn rimraf ./dist" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "eslint": "^8.45.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^5.0.0", + "json-schema-to-typescript": "^14.0.0", + "prettier": "^3.0.0", + "rimraf": "^5.0.1", + "ts-node": "^10.9.1", + "typescript": "^5.4.2" + } +} diff --git a/packages/catalog-generator/pom.xml b/packages/catalog-generator/pom.xml new file mode 100644 index 000000000..9414d11aa --- /dev/null +++ b/packages/catalog-generator/pom.xml @@ -0,0 +1,177 @@ + + + 4.0.0 + io.kaoto + catalog-generator + 0.0.1-SNAPSHOT + catalog-generator + A Camel Catalog generator for Kaoto. + https://kaoto.io + + 3.13.0 + 21 + 21 + 21 + UTF-8 + UTF-8 + 4.6.0 + 3.8.0 + 4.6.0 + 2.17.1 + 5.10.2 + 6.10.0 + 3.13.0 + 3.2.5 + + + + commons-io + commons-io + 2.15.1 + + + org.apache.camel + camel-catalog + ${version.camel} + + + org.apache.camel + camel-catalog-maven + ${version.camel} + + + org.apache.camel.springboot + camel-catalog-provider-springboot + ${version.camel} + + + org.apache.camel.quarkus + camel-quarkus-catalog + ${version.camel.quarkus} + + + org.apache.camel.kamelets + camel-kamelets + ${version.camel-kamelets} + + + io.fabric8 + kubernetes-model + ${version.kubernetes-model} + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${version.jackson} + + + commons-cli + commons-cli + 1.8.0 + + + org.junit.jupiter + junit-jupiter + ${version.junit} + test + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + true + io.kaoto.camelcatalog.Main + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + + io.kaoto.camelcatalog.Main + + + + + + + + cz.habarta.typescript-generator + typescript-generator-maven-plugin + 3.2.1263 + + + generate + process-classes + + generate + + + + + jackson2 + false + module + declarationFile + true + true + perResource + + ./dist/types/catalog-index.d.ts + + io.kaoto.camelcatalog.model.* + + + + + maven-compiler-plugin + ${version.maven-compiler-plugin} + + + testCompile + + testCompile + + + + + + maven-surefire-plugin + ${version.maven-surefire-plugin} + + + test + + test + + + + + + + diff --git a/packages/catalog-generator/scripts/json-schema-to-typescript.mts b/packages/catalog-generator/scripts/json-schema-to-typescript.mts new file mode 100644 index 000000000..ab87002f5 --- /dev/null +++ b/packages/catalog-generator/scripts/json-schema-to-typescript.mts @@ -0,0 +1,115 @@ +#!/usr/bin/env ts-node +/* eslint-disable @typescript-eslint/no-var-requires */ + +/** + * This script generates TypeScript types from the JSON schemas in the dist folder. + */ +import { mkdir, writeFile } from 'fs/promises'; +import { JSONSchema, compile } from 'json-schema-to-typescript'; +import { pathToFileURL } from 'node:url'; +import { resolve } from 'path'; +import { rimraf } from 'rimraf'; +import catalogLibraryIndex from '../dist/index.json' assert { type: 'json' }; +import { CatalogDefinition } from '../dist/types/catalog-index'; + +/** Function to ensure the dist/types folder is created and empty */ +const ensureTypesFolder = async () => { + const typesFolder = resolve('./dist/types'); + + await rimraf(typesFolder, { filter: (path) => !path.includes('catalog-index.d.ts') }); + await mkdir(typesFolder, { recursive: true }); +}; + +/** Function to compile a JSON schema file to a TypeScript file */ +const compileSchema = async (schemaContent: JSONSchema, name: string, outputFile: string) => { + const ts = await compile(schemaContent, name); + await writeFile(outputFile, ts); +}; + +/** + * Function to add a title property for schema properties that doesn't contains it + * The goal for this is to provide a better naming for the generated types + */ +const addTitleToDefinitions = (schema: JSONSchema) => { + if (!schema.items || Array.isArray(schema.items) || !schema.items.definitions) { + return; + } + + Object.entries(schema.items.definitions).forEach(([key, value]) => { + if (value.title) { + return; + } + + const title = key.split('.').slice(-1).join(''); + console.log(`\tAdding title to ${key}: ${title}`); + + value.title = title; + }); +}; + +/** Main function */ +async function main() { + await ensureTypesFolder(); + + const exportedFiles: string[] = ['catalog-index']; + + console.log('---'); + const targetSchemaNames = [ + 'camelYamlDsl', + 'Integration', + 'Kamelet', + 'KameletBinding', + 'Pipe', + 'ObjectMeta', + 'ObjectReference', + 'PipeErrorHandler', + ]; + + if (!Array.isArray(catalogLibraryIndex.definitions) || !catalogLibraryIndex.definitions.length) { + throw new Error('Invalid catalog index file, a Catalog needs to be generated first'); + } + + const indexDefinitionFileName = catalogLibraryIndex.definitions[0].fileName; + const indexFileUri = pathToFileURL(`./dist/${indexDefinitionFileName}`).toString(); + const indexDefinitionContent: CatalogDefinition = (await import(indexFileUri, { assert: { type: 'json' } })).default; + + const schemaPromises = Object.entries(indexDefinitionContent.schemas).map(async ([name, schema]) => { + if (!targetSchemaNames.includes(name)) { + return; + } + + const baseFolder = indexDefinitionFileName.substring(0, indexDefinitionFileName.lastIndexOf('/')); + const schemaFile = resolve(`./dist/${baseFolder}/${schema.file}`); + + /** + * In windows, path starting with C:\ are not supported + * We need to add file:// to the path to make it work + * [pathToFileURL](https://nodejs.org/api/url.html#url_url_pathtofileurl_path) + * Related issue: https://github.com/nodejs/node/issues/31710 + */ + const schemaFileUri = pathToFileURL(schemaFile).toString(); + const schemaContent = (await import(schemaFileUri, { assert: { type: 'json' } })).default; + + addTitleToDefinitions(schemaContent); + + /** Remove the -4.0.0.json section of the filename */ + const outputFile = resolve(`./dist/types/${name}.d.ts`); + + /** Add the file to the exported files */ + exportedFiles.push(name); + + console.log(`Input: '${schemaFile}'`); + console.log(`Output: ${outputFile}`); + console.log('---'); + + return compileSchema(schemaContent, name, outputFile); + }); + await Promise.all(schemaPromises); + + /** Generate the index file */ + const indexFile = resolve(`./dist/types/index.ts`); + const indexContent = exportedFiles.map((file) => `export * from './${file}';`).join('\n'); + await writeFile(indexFile, indexContent); +} + +main(); diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Build.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Build.java new file mode 100644 index 000000000..a8d2edb29 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Build.java @@ -0,0 +1,26 @@ +package io.kaoto.camelcatalog; + +import java.util.logging.Logger; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; + +import io.kaoto.camelcatalog.beans.ConfigBean; +import io.kaoto.camelcatalog.commands.GenerateCommand; +import io.kaoto.camelcatalog.commands.GenerateCommandOptions; + +public class Build { + private static final Logger LOGGER = Logger.getLogger(Build.class.getName()); + + public static void main(String[] args) { + CamelCatalog catalog = new DefaultCamelCatalog(); + String camelMainVersion = catalog.getCatalogVersion(); + + LOGGER.info("Building Camel Main Catalog: " + camelMainVersion); + + String[] localArgs = { "--output", "./dist", "--name", "Kaoto default catalog", "--main", camelMainVersion }; + ConfigBean configBean = GenerateCommandOptions.configure(localArgs); + GenerateCommand generateCommand = new GenerateCommand(configBean); + generateCommand.run(); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Main.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Main.java new file mode 100644 index 000000000..8937614da --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/Main.java @@ -0,0 +1,13 @@ +package io.kaoto.camelcatalog; + +import io.kaoto.camelcatalog.beans.ConfigBean; +import io.kaoto.camelcatalog.commands.GenerateCommand; +import io.kaoto.camelcatalog.commands.GenerateCommandOptions; + +public class Main { + public static void main(String[] args) { + ConfigBean configBean = GenerateCommandOptions.configure(args); + GenerateCommand generateCommand = new GenerateCommand(configBean); + generateCommand.run(); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/beans/ConfigBean.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/beans/ConfigBean.java new file mode 100644 index 000000000..42a64b22b --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/beans/ConfigBean.java @@ -0,0 +1,58 @@ +package io.kaoto.camelcatalog.beans; + +import java.io.File; +import java.util.LinkedHashSet; +import java.util.Set; + +import io.kaoto.camelcatalog.model.CatalogCliArgument; + +public class ConfigBean { + private File outputFolder; + private String catalogsName; + private Set catalogVersionSet = new LinkedHashSet<>(); + private String kameletsVersion; + private boolean verbose = false; + + public ConfigBean() { + } + + public File getOutputFolder() { + return outputFolder; + } + + public void setOutputFolder(String outputFolder) { + this.outputFolder = new File(outputFolder); + } + + public String getCatalogsName() { + return catalogsName; + } + + public void setCatalogsName(String catalogsName) { + this.catalogsName = catalogsName; + } + + public void addCatalogVersion(CatalogCliArgument catalogCliArg) { + this.catalogVersionSet.add(catalogCliArg); + } + + public Set getCatalogVersionSet() { + return catalogVersionSet; + } + + public String getKameletsVersion() { + return kameletsVersion; + } + + public void setKameletsVersion(String kameletsVersion) { + this.kameletsVersion = kameletsVersion; + } + + public boolean isVerbose() { + return verbose; + } + + public void setVerbose(boolean verbose) { + this.verbose = verbose; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommand.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommand.java new file mode 100644 index 000000000..e95c35442 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommand.java @@ -0,0 +1,87 @@ +package io.kaoto.camelcatalog.commands; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import io.kaoto.camelcatalog.beans.ConfigBean; +import io.kaoto.camelcatalog.generator.CatalogGeneratorBuilder; +import io.kaoto.camelcatalog.model.CatalogDefinition; +import io.kaoto.camelcatalog.model.CatalogLibrary; + +public class GenerateCommand implements Runnable { + private static final Logger LOGGER = Logger.getLogger(GenerateCommand.class.getName()); + private ConfigBean configBean; + + public GenerateCommand(ConfigBean configBean) { + this.configBean = configBean; + } + + @Override + public void run() { + LOGGER.info("Output folder: " + configBean.getOutputFolder() + "\n" + + "Catalog versions: " + configBean.getCatalogVersionSet() + "\n" + + "Kamelets version: " + configBean.getKameletsVersion()); + + CatalogLibrary library = new CatalogLibrary(); + library.setName(configBean.getCatalogsName()); + + FileUtils.deleteQuietly(configBean.getOutputFolder()); + File outputFolder = createSubFolder(configBean.getOutputFolder()); + + configBean.getCatalogVersionSet() + .forEach(catalogCliArg -> { + String runtimeFolderName = "camel-" + catalogCliArg.getRuntime().name().toLowerCase(); + File runtimeFolder = createSubFolder(outputFolder, runtimeFolderName); + File catalogDefinitionFolder = createSubFolder(runtimeFolder, catalogCliArg.getCatalogVersion()); + + LOGGER.info("Generating catalog: " + catalogCliArg.getRuntime() + " " + + catalogCliArg.getCatalogVersion() + "\n"); + + CatalogGeneratorBuilder builder = new CatalogGeneratorBuilder(); + var catalogGenerator = builder.withRuntime(catalogCliArg.getRuntime()) + .withCamelCatalogVersion(catalogCliArg.getCatalogVersion()) + .withKameletsVersion(configBean.getKameletsVersion()) + .withCamelKCRDsVersion("2.3.1") + .withOutputDirectory(catalogDefinitionFolder) + .build(); + + CatalogDefinition catalogDefinition = catalogGenerator.generate(); + File indexFile = catalogDefinitionFolder.toPath().resolve(catalogDefinition.getFileName()).toFile(); + File relateIndexFile = outputFolder.toPath().relativize(indexFile.toPath()).toFile(); + + catalogDefinition.setFileName(relateIndexFile.toString()); + + library.addDefinition(catalogDefinition); + }); + + ObjectMapper jsonMapper = new ObjectMapper() + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + + var indexFile = outputFolder.toPath().resolve("index.json").toFile(); + try { + jsonMapper.writerWithDefaultPrettyPrinter().writeValue(indexFile, library); + } catch (IOException e) { + throw new RuntimeException("Error writing index file", e); + } + + } + + private File createSubFolder(File parentFolder, String folderName) { + File newSubFolder = parentFolder.toPath().resolve(folderName).toFile(); + return createSubFolder(newSubFolder); + } + + private File createSubFolder(File parentFolder) { + File newSubFolder = parentFolder.toPath().toFile(); + if (!newSubFolder.exists()) { + newSubFolder.mkdirs(); + } + return newSubFolder; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommandOptions.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommandOptions.java new file mode 100644 index 000000000..62a6facde --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/commands/GenerateCommandOptions.java @@ -0,0 +1,125 @@ +package io.kaoto.camelcatalog.commands; + +import java.util.logging.Logger; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.catalog.quarkus.QuarkusRuntimeProvider; +import org.apache.camel.springboot.catalog.SpringBootRuntimeProvider; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import io.kaoto.camelcatalog.beans.ConfigBean; +import io.kaoto.camelcatalog.generator.Util; +import io.kaoto.camelcatalog.model.CatalogCliArgument; +import io.kaoto.camelcatalog.model.CatalogRuntime; + +public class GenerateCommandOptions { + private static final Logger LOGGER = Logger.getLogger(GenerateCommandOptions.class.getName()); + private static Options options = new Options(); + + public static ConfigBean configure(String[] args) { + Option outputOption = Option.builder().argName("outputDir").option("o").longOpt("output") + .desc("Output directory. It will be cleaned before generating the catalogs").hasArg() + .required() + .build(); + Option catalogsNameOption = Option.builder().argName("catalogsName").option("n").longOpt("name") + .desc("Catalog Name. It serves as human readable identifier of the catalog repository") + .hasArg() + .required() + .build(); + Option kameletsVersionOption = Option.builder().argName("kameletsVersion").option("k") + .longOpt("kamelets") + .desc("Kamelets catalog version. If not specified, it will use the generator installed version") + .hasArg().build(); + Option camelMainVersionOption = Option.builder().argName("version").option("m").longOpt("main") + .desc("Camel Main version. If not specified, it will use the generator installed version") + .hasArg().build(); + Option camelQuarkusVersionOption = Option.builder().argName("version").option("q").longOpt("quarkus") + .desc("Camel Extensions for Quarkus version").hasArg().build(); + Option camelSpringbootVersionOption = Option.builder().argName("version").option("s") + .longOpt("springboot") + .desc("Camel SpringBoot version").hasArg().build(); + Option verboseOption = Option.builder().argName("v").option("v").longOpt("verbose") + .desc("Be more verbose") + .build(); + + options.addOption(outputOption); + options.addOption(catalogsNameOption); + options.addOption(kameletsVersionOption); + options.addOption(camelMainVersionOption); + options.addOption(camelQuarkusVersionOption); + options.addOption(camelSpringbootVersionOption); + options.addOption(verboseOption); + + ConfigBean configBean = new ConfigBean(); + CommandLineParser parser = new DefaultParser(); + + CommandLine cmd = null; + try { + cmd = parser.parse(options, args); + configBean.setOutputFolder(Util.getNormalizedFolder(cmd.getOptionValue(outputOption.getOpt()))); + configBean.setCatalogsName(cmd.getOptionValue(catalogsNameOption.getOpt())); + configBean.setKameletsVersion(cmd.getOptionValue(kameletsVersionOption.getOpt())); + + addRuntimeVersions(configBean, cmd, camelMainVersionOption, CatalogRuntime.Main); + addRuntimeVersions(configBean, cmd, camelQuarkusVersionOption, CatalogRuntime.Quarkus); + addRuntimeVersions(configBean, cmd, camelSpringbootVersionOption, CatalogRuntime.SpringBoot); + + if (configBean.getCatalogVersionSet().isEmpty()) { + addDefaultVersions(configBean); + } + } catch (ParseException e) { + LOGGER.severe("Missing required options"); + e.printStackTrace(); + printHelpAndExit(); + } + + return configBean; + } + + private static void printHelpAndExit() { + printHelp(); + System.exit(1); + } + + private static void printHelp() { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("catalog-generator", options); + } + + private static void addRuntimeVersions(ConfigBean configBean, CommandLine cmd, Option option, + CatalogRuntime runtime) { + String[] versions = cmd.getOptionValues(option.getOpt()); + if (versions != null) { + for (String version : versions) { + configBean.addCatalogVersion(new CatalogCliArgument(runtime, version)); + } + } + } + + private static void addDefaultVersions(ConfigBean configBean) { + // If no version is specified, we will generate the main catalog with the + // installed version + LOGGER.warning("\nNo Camel Main catalog version specified. \nGenerating the main catalog with the installed version"); + + CamelCatalog camelCatalog = new DefaultCamelCatalog(); + configBean.addCatalogVersion(new CatalogCliArgument(CatalogRuntime.Main, camelCatalog.getCatalogVersion())); + + QuarkusRuntimeProvider quarkusRuntimeProvider = new QuarkusRuntimeProvider(); + camelCatalog.setRuntimeProvider(quarkusRuntimeProvider); + String quarkusYamlDslVersion = camelCatalog.otherModel("yaml-dsl").getVersion(); + configBean.addCatalogVersion(new CatalogCliArgument(CatalogRuntime.Quarkus, quarkusYamlDslVersion)); + + SpringBootRuntimeProvider springBootRuntimeProvider = new SpringBootRuntimeProvider(); + camelCatalog.setRuntimeProvider(springBootRuntimeProvider); + String springbootYamlDslVersion = camelCatalog.otherModel("yaml-dsl").getVersion(); + configBean.addCatalogVersion(new CatalogCliArgument(CatalogRuntime.SpringBoot, springbootYamlDslVersion)); + } + +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java new file mode 100644 index 000000000..e726e4c96 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelCatalogProcessor.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.generator; + +import java.io.InputStream; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.tooling.model.EipModel; +import org.apache.camel.tooling.model.JsonMapper; +import org.apache.camel.tooling.model.Kind; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Customize Camel Catalog for Kaoto. + */ +public class CamelCatalogProcessor { + private static final Logger LOGGER = Logger.getLogger(CamelCatalogProcessor.class.getName()); + + private static final String TO_DYNAMIC_DEFINITION = "org.apache.camel.model.ToDynamicDefinition"; + private static final String SET_HEADERS_DEFINITION = "org.apache.camel.model.SetHeadersDefinition"; + private static final String SET_VARIABLES_DEFINITION = "org.apache.camel.model.SetVariablesDefinition"; + private final ObjectMapper jsonMapper; + private final CamelCatalog camelCatalog; + private final CamelYamlDslSchemaProcessor schemaProcessor; + private boolean verbose; + + public CamelCatalogProcessor(CamelCatalog camelCatalog, ObjectMapper jsonMapper, + CamelYamlDslSchemaProcessor schemaProcessor, boolean verbose) { + this.jsonMapper = jsonMapper; + this.camelCatalog = camelCatalog; + this.schemaProcessor = schemaProcessor; + this.verbose = verbose; + } + + /** + * Create Camel catalogs customized for Kaoto usage. + * + * @return + */ + public Map processCatalog() throws Exception { + var answer = new LinkedHashMap(); + var componentCatalog = getComponentCatalog(); + var dataFormatCatalog = getDataFormatCatalog(); + var languageCatalog = getLanguageCatalog(); + var modelCatalog = getModelCatalog(); + var patternCatalog = getPatternCatalog(); + var entityCatalog = getEntityCatalog(); + var loadBalancerCatalog = getLoadBalancerCatalog(); + answer.put("components", componentCatalog); + answer.put("dataformats", dataFormatCatalog); + answer.put("languages", languageCatalog); + answer.put("models", modelCatalog); + answer.put("patterns", patternCatalog); + answer.put("entities", entityCatalog); + answer.put("loadbalancers", loadBalancerCatalog); + return answer; + } + + /** + * Get aggregated Camel component Catalog. + * + * @return + * @throws Exception + */ + public String getComponentCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + camelCatalog.findComponentNames().stream().filter(component -> !component.isEmpty()).sorted() + .forEach((name) -> { + try { + var model = (ComponentModel) camelCatalog.model(Kind.component, name); + var json = JsonMapper.asJsonObject(model).toJson(); + var catalogNode = (ObjectNode) jsonMapper.readTree(json); + generatePropertiesSchema(catalogNode); + answer.set(name, catalogNode); + } catch (Exception e) { + if (verbose) { + LOGGER.warning("The component definition for " + name + " is null.\n" + e.getMessage()); + } + } + }); + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + private void generatePropertiesSchema(ObjectNode parent) throws Exception { + var answer = parent.withObject("/propertiesSchema"); + answer.put("$schema", "http://json-schema.org/draft-07/schema#"); + answer.put("type", "object"); + + var properties = parent.withObject("/properties"); + var answerProperties = answer.withObject("/properties"); + var required = new LinkedHashSet(); + for (var propertyEntry : properties.properties()) { + var propertyName = propertyEntry.getKey(); + var property = (ObjectNode) propertyEntry.getValue(); + var propertySchema = answerProperties.withObject("/" + propertyName); + if (property.has("displayName")) + propertySchema.put("title", property.get("displayName").asText()); + if (property.has("group")) + propertySchema.put("group", property.get("group").asText()); + if (property.has("description")) + propertySchema.put("description", property.get("description").asText()); + var propertyType = "string"; + if (property.has("type")) { + propertyType = property.get("type").asText(); + if ("duration".equals(propertyType)) { + propertyType = "string"; + propertySchema.put("format", "duration"); + } + propertySchema.put("type", propertyType); + } + if (property.has("deprecated")) + propertySchema.put("deprecated", property.get("deprecated").asBoolean()); + if (property.has("secret") && property.get("secret").asBoolean()) { + propertySchema.put("format", "password"); + } + if (property.has("required") && property.get("required").asBoolean()) { + required.add(propertyName); + } + if (property.has("defaultValue")) { + if ("array".equals(propertyType)) { + propertySchema.withArray("/default").add(property.get("defaultValue")); + } else { + propertySchema.set("default", property.get("defaultValue")); + } + } + + if (property.has("enum")) { + property.withArray("/enum") + .forEach(e -> propertySchema.withArray("/enum").add(e)); + if (!propertySchema.has("type") || "object".equals(propertySchema.get("type").asText())) { + propertySchema.put("type", "string"); + } + } else if ("array".equals(propertyType)) { + propertySchema.withObject("/items").put("type", "string"); + } else if ("object".equals(propertyType) && property.has("javaType") + && !property.get("javaType").asText().startsWith("java.util.Map")) { + // Put "string" as a type and javaType as a schema $comment to indicate + // that the UI should handle this as a bean reference field + propertySchema.put("type", "string"); + propertySchema.put("$comment", "class:" + property.get("javaType").asText()); + } + } + required.forEach(req -> answer.withArray("/required").add(req)); + } + + /** + * Get aggregated Camel DataFormat catalog with a custom dataformat added. + * + * @return + * @throws Exception + */ + public String getDataFormatCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + var dataFormatSchemaMap = schemaProcessor.getDataFormats(); + for (var entry : dataFormatSchemaMap.entrySet()) { + var dataFormatName = entry.getKey(); + var dataFormatSchema = entry.getValue(); + var dataFormatCatalog = (EipModel) camelCatalog.model(Kind.eip, dataFormatName); + if (dataFormatCatalog == null) { + throw new Exception("DataFormat " + dataFormatName + " is not found in Camel model catalog."); + } + var json = JsonMapper.asJsonObject(dataFormatCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + catalogTree.set("propertiesSchema", dataFormatSchema); + answer.set(dataFormatName, catalogTree); + } + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + /** + * Get Camel language catalog with a custom language added. + * + * @return + * @throws Exception + */ + public String getLanguageCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + var languageSchemaMap = schemaProcessor.getLanguages(); + for (var entry : languageSchemaMap.entrySet()) { + var languageName = entry.getKey(); + var languageSchema = entry.getValue(); + var languageCatalog = (EipModel) camelCatalog.model(Kind.eip, languageName); + if (languageCatalog == null) { + throw new Exception("Language " + languageName + " is not found in Camel model catalog."); + } + var json = JsonMapper.asJsonObject(languageCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + catalogTree.set("propertiesSchema", languageSchema); + answer.set(languageName, catalogTree); + } + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + public String getModelCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + camelCatalog.findModelNames().stream().sorted().forEach((name) -> { + try { + var model = (EipModel) camelCatalog.model(Kind.eip, name); + var json = JsonMapper.asJsonObject(model).toJson(); + var catalogNode = (ObjectNode) jsonMapper.readTree(json); + if ("from".equals(name)) { + // "from" is an exception that is not a processor, therefore it's not in the + // pattern catalog - put the propertiesSchema here + generatePropertiesSchema(catalogNode); + } + answer.set(name, catalogNode); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + /** + * Get a Camel processor model catalog filtered from model catalog by comparing + * with YAML DSL schema. + * + * @return + * @throws Exception + */ + public String getPatternCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + var processors = schemaProcessor.getProcessors(); + var catalogMap = new LinkedHashMap(); + for (var name : camelCatalog.findModelNames()) { + var modelCatalog = (EipModel) camelCatalog.model(Kind.eip, name); + catalogMap.put(modelCatalog.getJavaType(), modelCatalog); + } + + for (var entry : processors.entrySet()) { + var sortedSchemaProperties = jsonMapper.createObjectNode(); + var processorFQCN = entry.getKey(); + var processorSchema = entry.getValue(); + var processorCatalog = catalogMap.get(processorFQCN); + List required = new ArrayList<>(); + + var camelYamlDslProperties = processorSchema.withObject("/properties").properties().stream() + .map(Map.Entry::getKey).sorted( + new CamelYamlDSLKeysComparator(processorCatalog.getOptions())) + .toList(); + + for (var propertyName : camelYamlDslProperties) { + var propertySchema = processorSchema.withObject("/properties").withObject("/" + propertyName); + if (TO_DYNAMIC_DEFINITION.equals(processorFQCN) && "parameters".equals(propertyName)) { + // "parameters" as a common property is omitted in the catalog, but we need this + // for "toD" + propertySchema.put("title", "Parameters"); + propertySchema.put("description", "URI parameters"); + sortedSchemaProperties.set(propertyName, propertySchema); + continue; + } + if (SET_HEADERS_DEFINITION.equals((processorFQCN)) && "headers".equals(propertyName)) { + propertySchema.put("title", "Headers"); + propertySchema.put("description", "Headers to set"); + sortedSchemaProperties.set(propertyName, propertySchema); + continue; + } + if (SET_VARIABLES_DEFINITION.equals((processorFQCN)) && "variables".equals(propertyName)) { + propertySchema.put("title", "Variables"); + propertySchema.put("description", "Variables to set"); + sortedSchemaProperties.set(propertyName, propertySchema); + continue; + } + + var catalogOpOptional = processorCatalog.getOptions().stream() + .filter(op -> op.getName().equals(propertyName)).findFirst(); + if (catalogOpOptional.isEmpty()) { + throw new Exception( + String.format("Option '%s' not found for processor '%s'", propertyName, processorFQCN)); + } + var catalogOp = catalogOpOptional.get(); + if ("object".equals(catalogOp.getType()) && !catalogOp.getJavaType().startsWith("java.util.Map") + && !propertySchema.has("$comment")) { + propertySchema.put("$comment", "class:" + catalogOp.getJavaType()); + } + if (catalogOp.isRequired()) { + required.add(propertyName); + } + + sortedSchemaProperties.set(propertyName, propertySchema); + } + + var json = JsonMapper.asJsonObject(processorCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + catalogTree.set("propertiesSchema", processorSchema); + catalogTree.withObject("/propertiesSchema").set("required", jsonMapper.valueToTree(required)); + processorSchema.set("properties", sortedSchemaProperties); + answer.set(processorCatalog.getName(), catalogTree); + } + + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + /** + * Get a Camel entity catalog filtered from model catalog, then combine the + * corresponding part of + * Camel YAML DSL JSON schema as a `propertiesSchema` in the usable format for + * uniforms to render + * the configuration form. "entity" here means the top level properties in Camel + * YAML DSL, such as + * "route", "rest", "beans", "routeConfiguration", etc. They are marked with + * "@YamlIn" annotation + * in the Camel codebase. + * This also adds `routeTemplateBean` and `templatedRouteBean` separately. + * `routeTemplateBean` is + * also used for Kamelet. + * + * @return + * @throws Exception + */ + public String getEntityCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + var entities = schemaProcessor.getEntities(); + var catalogMap = new LinkedHashMap(); + for (var name : camelCatalog.findModelNames()) { + var modelCatalog = (EipModel) camelCatalog.model(Kind.eip, name); + catalogMap.put(name, modelCatalog); + } + InputStream is = camelCatalog.getClass().getClassLoader() + .getResourceAsStream("org/apache/camel/catalog/models-app/bean.json"); + var beanJsonObj = JsonMapper.deserialize(new String(is.readAllBytes())); + var beanModel = JsonMapper.generateEipModel(beanJsonObj); + catalogMap.put("beans", beanModel); + for (var entry : entities.entrySet()) { + var entityName = entry.getKey(); + var entitySchema = entry.getValue(); + var entityCatalog = catalogMap.get(entityName); + switch (entityName) { + case "beans" -> processBeansParameters(entitySchema, entityCatalog); + case "from" -> processFromParameters(entitySchema, entityCatalog); + case "route" -> processRouteParameters(entitySchema, entityCatalog); + case "routeTemplate" -> processRouteTemplateParameters(entitySchema, entityCatalog); + case "templatedRoute" -> processTemplatedRouteParameters(entitySchema, entityCatalog); + case "restConfiguration" -> processRestConfigurationParameters(entitySchema, entityCatalog); + case "rest" -> processRestParameters(entitySchema, entityCatalog); + case null, default -> processEntityParameters(entityName, entitySchema, entityCatalog); + } + + sortPropertiesAccordingToCamelCatalog(entitySchema, entityCatalog); + + var json = JsonMapper.asJsonObject(entityCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + catalogTree.set("propertiesSchema", entitySchema); + answer.set(entityName, catalogTree); + } + addMoreBeans(answer, catalogMap); + + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } + + private void doProcessParameter(EipModel entityCatalog, String propertyName, ObjectNode propertySchema) + throws Exception { + var catalogOp = entityCatalog.getOptions().stream().filter(op -> op.getName().equals(propertyName)).findFirst(); + if (catalogOp.isEmpty()) { + throw new Exception(String.format("Option '%s' not found for '%s'", propertyName, entityCatalog.getName())); + } + var catalogOption = catalogOp.get(); + if (catalogOption.getDisplayName() != null) + propertySchema.put("title", catalogOption.getDisplayName()); + if (catalogOption.getDescription() != null) + propertySchema.put("description", catalogOption.getDescription()); + var propertyType = propertySchema.has("type") ? propertySchema.get("type").asText() : null; + if (catalogOption.getDefaultValue() != null) { + if ("array".equals(propertyType)) { + propertySchema.withArray("/default").add(catalogOption.getDefaultValue().toString()); + } else if ("boolean".equals(propertyType)) { + propertySchema.put("default", Boolean.valueOf(catalogOption.getDefaultValue().toString())); + } else { + propertySchema.put("default", catalogOption.getDefaultValue().toString()); + } + } + // if the enum is defined in YAML DSL schema, honor that, otherwise copy from + // the catalog. + if (catalogOption.getEnums() != null && !propertySchema.has("enum")) { + catalogOption.getEnums() + .forEach(e -> propertySchema.withArray("/enum").add(e)); + if (!propertySchema.has("type") || "object".equals(propertySchema.get("type").asText())) { + propertySchema.put("type", "string"); + } + } + } + + private void processBeansParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + var beanDef = entitySchema.withObject("/definitions") + .withObject("/org.apache.camel.model.BeanFactoryDefinition"); + for (var property : beanDef.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processFromParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("parameters".equals(propertyName)) { + // "parameters" as a common property is omitted in the catalog, but we need this + // for "from" + propertySchema.put("title", "Parameters"); + propertySchema.put("description", "URI parameters"); + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processRouteParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var op : entityCatalog.getOptions()) { + // parameter name mismatch between schema and catalog + if ("routePolicyRef".equals(op.getName())) { + op.setName("routePolicy"); + } + } + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("from".equals(propertyName)) { + // no "from" in the catalog + propertySchema.put("title", "From"); + propertySchema.put("description", "From"); + continue; + } else if (List.of("inputType", "outputType").contains(propertyName)) { + // no "inputType" and "outputType" in the catalog, just keep it as-is + continue; + } else if ("streamCaching".equals(propertyName)) { + entityCatalog.getOptions().stream().filter(op -> "streamCache".equals(op.getName())).findFirst() + .ifPresent(op -> { + op.setName("streamCaching"); + }); + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processRouteTemplateParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var op : entityCatalog.getOptions()) { + // parameter name mismatch between schema and catalog + if ("templateBean".equals(op.getName())) { + op.setName("beans"); + } + } + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("from".equals(propertyName)) { + // no "from" in the catalog + propertySchema.put("title", "From"); + propertySchema.put("description", "From"); + continue; + } else if ("parameters".equals(propertyName)) { + // "parameters" as a common property is omitted in the catalog, but we need this + // for "from" + propertySchema.put("title", "Parameters"); + propertySchema.put("description", "URI parameters"); + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processTemplatedRouteParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var op : entityCatalog.getOptions()) { + // parameter name mismatch between schema and catalog + if ("bean".equals(op.getName())) { + op.setName("beans"); + } + } + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("from".equals(propertyName)) { + // no "from" in the catalog + propertySchema.put("title", "From"); + propertySchema.put("description", "From"); + continue; + } else if ("parameters".equals(propertyName)) { + // "parameters" as a common property is omitted in the catalog, but we need this + // for "from" + propertySchema.put("title", "Parameters"); + propertySchema.put("description", "URI parameters"); + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processRestConfigurationParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("enableCors".equals(propertyName)) { + // no "from" in the catalog + propertySchema.put("title", "Enable CORS"); + propertySchema.put("description", "Enable CORS"); + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processRestParameters(ObjectNode entitySchema, EipModel entityCatalog) throws Exception { + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("enableCors".equals(propertyName)) { + // no "from" in the catalog + propertySchema.put("title", "Enable CORS"); + propertySchema.put("description", "Enable CORS"); + continue; + } else if (List.of("get", "post", "put", "patch", "delete", "head").contains(propertyName)) { + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void processEntityParameters(String entityName, ObjectNode entitySchema, EipModel entityCatalog) + throws Exception { + for (var property : entitySchema.withObject("/properties").properties()) { + var propertyName = property.getKey(); + var propertySchema = (ObjectNode) property.getValue(); + if ("from".equals(entityName) && "parameters".equals(propertyName)) { + // "parameters" as a common property is omitted in the catalog, but we need this + // for "from" + propertySchema.put("title", "Parameters"); + propertySchema.put("description", "URI parameters"); + continue; + } + doProcessParameter(entityCatalog, propertyName, propertySchema); + } + } + + private void sortPropertiesAccordingToCamelCatalog(ObjectNode entitySchema, EipModel entityCatalog) { + var sortedSchemaProperties = jsonMapper.createObjectNode(); + var camelYamlDslProperties = entitySchema.withObject("/properties").properties().stream().map(Map.Entry::getKey) + .sorted( + new CamelYamlDSLKeysComparator(entityCatalog.getOptions())) + .toList(); + + for (var propertyName : camelYamlDslProperties) { + var propertySchema = entitySchema.withObject("/properties").withObject("/" + propertyName); + sortedSchemaProperties.set(propertyName, propertySchema); + } + + entitySchema.set("properties", sortedSchemaProperties); + } + + private void addMoreBeans(ObjectNode answer, Map catalogMap) throws Exception { + var beansCatalog = catalogMap.get("beans"); + var json = JsonMapper.asJsonObject(beansCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + var beanDefinition = answer.withObject("/beans") + .withObject("/propertiesSchema") + .withObject("/definitions") + .withObject("/org.apache.camel.model.BeanFactoryDefinition"); + catalogTree.set("propertiesSchema", beanDefinition); + answer.set("bean", catalogTree); + + // routeTemplateBean is used for kamelet beans in UI. Let BeanFactoryDefinition + // pretend to be routeTemplateBean + // definition in order to distinguish kamelet beans from plain YAML beans + // without making bigger UI change. + answer.set("routeTemplateBean", catalogTree); + } + + /** + * Get Camel LoadBalancer catalog with a custom loadbalancer added. + * + * @return + * @throws Exception + */ + public String getLoadBalancerCatalog() throws Exception { + var answer = jsonMapper.createObjectNode(); + var loadBalancerSchemaMap = schemaProcessor.getLoadBalancers(); + for (var entry : loadBalancerSchemaMap.entrySet()) { + var loadBalancerName = entry.getKey(); + var loadBalancerSchema = entry.getValue(); + var loadBalancerCatalog = (EipModel) camelCatalog.model(Kind.eip, loadBalancerName); + if (loadBalancerCatalog == null) { + throw new Exception("LoadBalancer " + loadBalancerName + " is not found in Camel model catalog."); + } + var json = JsonMapper.asJsonObject(loadBalancerCatalog).toJson(); + var catalogTree = (ObjectNode) jsonMapper.readTree(json); + catalogTree.set("propertiesSchema", loadBalancerSchema); + answer.set(loadBalancerName, catalogTree); + } + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, answer); + return writer.toString(); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparator.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparator.java new file mode 100644 index 000000000..ce0468c1f --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDSLKeysComparator.java @@ -0,0 +1,27 @@ +package io.kaoto.camelcatalog.generator; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +import org.apache.camel.tooling.model.EipModel.EipOptionModel; + +public class CamelYamlDSLKeysComparator implements Comparator { + + private final List eipOptions; + + CamelYamlDSLKeysComparator(List eipOptions) { + this.eipOptions = eipOptions; + } + + @Override + public int compare(String firstKey, String secondKey) { + Optional firstOption = eipOptions.stream().filter(e -> e.getName().equals(firstKey)).findFirst(); + Optional secondOption = eipOptions.stream().filter(e -> e.getName().equals(secondKey)).findFirst(); + + var firstIndex = firstOption.isPresent() ? firstOption.get().getIndex() : Integer.MAX_VALUE; + var secondIndex = secondOption.isPresent() ? secondOption.get().getIndex() : Integer.MAX_VALUE; + + return Integer.compare(firstIndex, secondIndex); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java new file mode 100644 index 000000000..cc4e88f9b --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CamelYamlDslSchemaProcessor.java @@ -0,0 +1,648 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.generator; + +import java.io.StringWriter; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Process camelYamlDsl.json file, aka Camel YAML DSL JSON schema. + */ +public class CamelYamlDslSchemaProcessor { + private static final String PROCESSOR_DEFINITION = "org.apache.camel.model.ProcessorDefinition"; + private static final String ROUTE_CONFIGURATION_DEFINITION = "org.apache.camel.model.RouteConfigurationDefinition"; + private static final String LOAD_BALANCE_DEFINITION = "org.apache.camel.model.LoadBalanceDefinition"; + private static final String EXPRESSION_SUB_ELEMENT_DEFINITION = "org.apache.camel.model.ExpressionSubElementDefinition"; + private static final String SAGA_DEFINITION = "org.apache.camel.model.SagaDefinition"; + private static final String PROPERTY_EXPRESSION_DEFINITION = "org.apache.camel.model.PropertyExpressionDefinition"; + private static final String ERROR_HANDLER_DEFINITION = "org.apache.camel.model.ErrorHandlerDefinition"; + private static final String ERROR_HANDLER_DESERIALIZER = "org.apache.camel.dsl.yaml.deserializers.ErrorHandlerBuilderDeserializer"; + private final ObjectMapper jsonMapper; + private final ObjectNode yamlDslSchema; + private final List processorBlocklist = List.of( + "org.apache.camel.model.KameletDefinition" + // reactivate entries once we have a better handling of how to add WHEN and + // OTHERWISE without Catalog + // "Otherwise", + // "when", + // "doCatch", + // ""doFinally" + ); + /** + * The processor properties those should be handled separately, i.e. remove from + * the properties schema, + * such as branching node and parameters reflected from the underlying + * components. + */ + private final Map> processorPropertyBlockList = Map.of( + "org.apache.camel.model.ChoiceDefinition", + List.of("when", "otherwise"), + "org.apache.camel.model.TryDefinition", + List.of("doCatch", "doFinally"), + "org.apache.camel.model.ToDefinition", + List.of("uri", "parameters"), + "org.apache.camel.model.WireTapDefinition", + List.of("uri", "parameters")); + private final List processorReferenceBlockList = List.of( + PROCESSOR_DEFINITION); + + public CamelYamlDslSchemaProcessor(ObjectMapper mapper, ObjectNode yamlDslSchema) throws Exception { + this.jsonMapper = mapper; + this.yamlDslSchema = yamlDslSchema; + } + + public Map processSubSchema() throws Exception { + var answer = new LinkedHashMap(); + var items = yamlDslSchema.withObject("/items"); + var properties = items.withObject("/properties"); + var definitions = items.withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + properties.properties().forEach(p -> { + var subSchema = doProcessSubSchema(p, relocatedDefinitions, yamlDslSchema); + answer.put(p.getKey(), subSchema); + }); + return answer; + } + + private ObjectNode relocateToRootDefinitions(ObjectNode definitions) { + var relocatedDefinitions = definitions.deepCopy(); + relocatedDefinitions.findParents("$ref").stream() + .map(ObjectNode.class::cast) + .forEach(n -> n.put("$ref", getRelocatedRef(n))); + return relocatedDefinitions; + } + + private String getRelocatedRef(ObjectNode parent) { + return parent.get("$ref").asText().replace("#/items/definitions/", "#/definitions/"); + } + + private String doProcessSubSchema( + java.util.Map.Entry prop, + ObjectNode definitions, + ObjectNode rootSchema) { + var answer = (ObjectNode) prop.getValue().deepCopy(); + if (answer.has("$ref") && definitions.has(getNameFromRef(answer))) { + answer = definitions.withObject("/" + getNameFromRef(answer)).deepCopy(); + + } + extractSingleOneOfFromAnyOf(answer); + answer.set("$schema", rootSchema.get("$schema")); + populateDefinitions(answer, definitions); + var writer = new StringWriter(); + try { + JsonGenerator gen = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(gen, answer); + return writer.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private String getNameFromRef(ObjectNode parent) { + var ref = parent.get("$ref").asText(); + return ref.contains("items") ? ref.replace("#/items/definitions/", "") + : ref.replace("#/definitions/", ""); + } + + private void populateDefinitions(ObjectNode schema, ObjectNode definitions) { + boolean added = true; + while (added) { + added = false; + for (JsonNode refParent : schema.findParents("$ref")) { + var name = getNameFromRef((ObjectNode) refParent); + if (processorReferenceBlockList.contains(name)) { + continue; + } + if (!schema.has("definitions") || !schema.withObject("/definitions").has(name)) { + var schemaDefinitions = schema.withObject("/definitions"); + schemaDefinitions.set(name, definitions.withObject("/" + name)); + added = true; + break; + } + } + } + } + + /** + * Extract single OneOf definition from AnyOf definition and put it into the + * root definitions. + * It's a workaround for the current Camel YAML DSL JSON schema, where some + * AnyOf definition + * contains only one OneOf definition. This can be removed once + * https://github.com/KaotoIO/kaoto/issues/948 + * is resolved. + * This is done mostly for the errorHandler definition, f.i. + * ``` + * { + * anyOf: [ + * { + * oneOf: [ + * { type: "object", ... }, + * { type: "object", ... }, + * ] + * }, + * ] + * } + * ``` + * will be transformed into + * ``` + * { + * oneOf: [ + * { type: "object", ... }, + * { type: "object", ... }, + * ] + * } + */ + private void extractSingleOneOfFromAnyOf(ObjectNode definition) { + if (!definition.has("anyOf")) { + return; + } + var anyOfArray = definition.withArray("/anyOf"); + if (anyOfArray.size() != 1) { + return; + } + + var anyOfOneOf = anyOfArray.get(0).withArray("/oneOf"); + definition.set("oneOf", anyOfOneOf); + definition.remove("anyOf"); + } + + /** + * Extract the processor definitions from the main Camel YAML DSL JSON schema in + * the usable + * format for uniforms to render the configuration form. It does a couple of + * things: + *
    + *
  • Remove "oneOf" and "anyOf"
  • + *
  • Remove properties those are supposed to be handled separately: + *
      + *
    • "steps": branching steps
    • + *
    • "parameters": component parameters
    • + *
    • expression languages
    • + *
    • dataformats
    • + *
    + *
  • + *
  • If the processor is expression aware, it puts "expression" as a + * "$comment" in the schema
  • + *
  • If the processor is dataformat aware, it puts "dataformat" as a + * "$comment" in the schema
  • + *
  • If the processor property is expression aware, it puts "expression" as a + * "$comment" in the property schema
  • + *
+ * + * @return + */ + public Map getProcessors() throws Exception { + var definitions = yamlDslSchema + .withObject("/items") + .withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + var processors = relocatedDefinitions + .withObject(PROCESSOR_DEFINITION) + .withObject("/properties"); + addRouteConfigurationProcessors(relocatedDefinitions, processors); + + var answer = new LinkedHashMap(); + for (var processorEntry : processors) { + var processorFQCN = getNameFromRef((ObjectNode) processorEntry); + if (processorBlocklist.contains(processorFQCN)) { + continue; + } + var processor = relocatedDefinitions.withObject("/" + processorFQCN); + processor = extractFromOneOf(processorFQCN, processor); + processor.remove("oneOf"); + processor = extractFromAnyOfOneOf(processorFQCN, processor); + processor.remove("anyOf"); + var processorProperties = processor.withObject("/properties"); + Set propToRemove = new HashSet<>(); + var propertyBlockList = processorPropertyBlockList.get(processorFQCN); + for (var propEntry : processorProperties.properties()) { + var propName = propEntry.getKey(); + if (propertyBlockList != null && propertyBlockList.contains(propName)) { + propToRemove.add(propName); + continue; + } + + var property = (ObjectNode) propEntry.getValue(); + var refParent = property.findParent("$ref"); + if (refParent != null) { + var ref = getNameFromRef(refParent); + if (processorReferenceBlockList.contains(ref)) { + if (processor.has("$comment")) { + processor.put("$comment", processor.get("$comment").asText() + ",steps"); + } else { + processor.put("$comment", "steps"); + } + propToRemove.add(propName); + } + if (EXPRESSION_SUB_ELEMENT_DEFINITION.equals(ref)) { + refParent.remove("$ref"); + refParent.put("type", "object"); + refParent.put("$comment", "expression"); + } + continue; + } + if (!property.has("type")) { + // inherited properties, such as for expression - supposed to be handled + // separately + propToRemove.add(propName); + } + } + propToRemove.forEach(processorProperties::remove); + populateDefinitions(processor, relocatedDefinitions); + sanitizeDefinitions(processorFQCN, processor); + answer.put(processorFQCN, processor); + } + return answer; + } + + private void addRouteConfigurationProcessors(ObjectNode relocatedDefinitions, ObjectNode processors) { + var routeConfigurationProcessor = relocatedDefinitions + .withObject(ROUTE_CONFIGURATION_DEFINITION) + .withObject("/properties"); + var interceptProcessor = routeConfigurationProcessor.withObject("intercept").withObject("items") + .withObject("properties"); + var interceptFromProcessor = routeConfigurationProcessor.withObject("interceptFrom").withObject("items") + .withObject("properties"); + var interceptSendToEndpointProcessor = routeConfigurationProcessor.withObject("interceptSendToEndpoint") + .withObject("items").withObject("properties"); + var onExceptionProcessor = routeConfigurationProcessor.withObject("onException").withObject("items") + .withObject("properties"); + var onCompletionProcessor = routeConfigurationProcessor.withObject("onCompletion").withObject("items") + .withObject("properties"); + processors.setAll(interceptProcessor); + processors.setAll(interceptFromProcessor); + processors.setAll(interceptSendToEndpointProcessor); + processors.setAll(onExceptionProcessor); + processors.setAll(onCompletionProcessor); + } + + private ObjectNode extractFromOneOf(String name, ObjectNode definition) throws Exception { + if (!definition.has("oneOf")) { + return definition; + } + var oneOf = definition.withArray("/oneOf"); + if (oneOf.size() != 2) { + throw new Exception(String.format( + "Definition '%s' has '%s' entries in oneOf unexpectedly, look it closer", + name, + oneOf.size())); + } + for (var def : oneOf) { + if (def.get("type").asText().equals("object")) { + var objectDef = (ObjectNode) def; + if (definition.has("title")) + objectDef.set("title", definition.get("title")); + if (definition.has("description")) + objectDef.set("description", definition.get("description")); + return objectDef; + } + } + throw new Exception(String.format( + "Definition '%s' oneOf doesn't have object entry unexpectedly, look it closer", + name)); + } + + private ObjectNode extractFromAnyOfOneOf(String name, ObjectNode definition) throws Exception { + if (!definition.has("anyOf")) { + return definition; + } + var anyOfOneOf = definition.withArray("/anyOf").get(0).withArray("/oneOf"); + for (var def : anyOfOneOf) { + if (def.has("$ref") && def.get("$ref").asText() + .equals("#/definitions/org.apache.camel.model.language.ExpressionDefinition")) { + definition.put("$comment", "expression"); + break; + } + var refParent = def.findParent("$ref"); + if (refParent != null + && refParent.get("$ref").asText().startsWith("#/definitions/org.apache.camel.model.dataformat")) { + definition.put("$comment", "dataformat"); + break; + } + if (LOAD_BALANCE_DEFINITION.equals(name)) { + definition.put("$comment", "loadbalance"); + break; + } + if (List.of(ERROR_HANDLER_DEFINITION, ERROR_HANDLER_DESERIALIZER).contains(name)) { + definition.put("$comment", "errorhandler"); + break; + } + } + definition.remove("anyOf"); + return definition; + } + + private void sanitizeDefinitions(String processorFQCN, ObjectNode processor) throws Exception { + if (!processor.has("definitions")) { + return; + } + var definitions = processor.withObject("/definitions"); + var defToRemove = new HashSet(); + for (var entry : definitions.properties()) { + var definitionName = entry.getKey(); + if (SAGA_DEFINITION.equals(processorFQCN) && definitionName.startsWith("org.apache.camel.language")) { + defToRemove.add(definitionName); + continue; + } + + var definition = (ObjectNode) entry.getValue(); + definition = extractFromOneOf(definitionName, definition); + definition = extractFromAnyOfOneOf(definitionName, definition); + var definitionProperties = definition.withObject("/properties"); + var propToRemove = new HashSet(); + for (var property : definitionProperties.properties()) { + var propName = property.getKey(); + var propValue = property.getValue(); + if (!propValue.has("$ref") && !propValue.has("type")) { + // inherited properties, such as for expression - supposed to be handled + // separately + propToRemove.add(propName); + } + + } + propToRemove.forEach(definitionProperties::remove); + if (PROPERTY_EXPRESSION_DEFINITION.equals(definitionName)) { + var expression = definition.withObject("/properties").withObject("/expression"); + expression.put("title", "Expression"); + expression.put("type", "object"); + expression.put("$comment", "expression"); + } + definitions.set(definitionName, definition); + } + defToRemove.forEach(definitions::remove); + } + + public Map getDataFormats() throws Exception { + var definitions = yamlDslSchema + .withObject("/items") + .withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + var fromMarshal = relocatedDefinitions + .withObject("/org.apache.camel.model.MarshalDefinition") + .withArray("/anyOf") + .get(0).withArray("/oneOf"); + var fromUnmarshal = relocatedDefinitions + .withObject("/org.apache.camel.model.UnmarshalDefinition") + .withArray("/anyOf") + .get(0).withArray("/oneOf"); + if (fromMarshal.size() != fromUnmarshal.size()) { + // Could this happen in the future? If so, we need to prepare separate sets for + // marshal and unmarshal + throw new Exception("Marshal and Unmarshal dataformats are not the same size"); + } + ; + + var answer = new LinkedHashMap(); + for (var entry : fromMarshal) { + if (!entry.has("required")) { + // assuming "not" entry + continue; + } + var entryName = entry.withArray("/required").get(0).asText(); + var property = entry + .withObject("/properties") + .withObject("/" + entryName); + var entryDefinitionName = getNameFromRef(property); + var dataformat = relocatedDefinitions.withObject("/" + entryDefinitionName); + if (!dataformat.has("oneOf")) { + populateDefinitions(dataformat, relocatedDefinitions); + answer.put(entryName, dataformat); + continue; + } + + var dfOneOf = dataformat.withArray("/oneOf"); + if (dfOneOf.size() != 2) { + throw new Exception(String.format( + "DataFormat '%s' has '%s' entries in oneOf unexpectedly, look it closer", + entryDefinitionName, + dfOneOf.size())); + } + for (var def : dfOneOf) { + if (def.get("type").asText().equals("object")) { + var objectDef = (ObjectNode) def; + objectDef.set("title", dataformat.get("title")); + objectDef.set("description", dataformat.get("description")); + populateDefinitions(objectDef, relocatedDefinitions); + answer.put(entryName, objectDef); + break; + } + } + } + return answer; + } + + public Map getLanguages() throws Exception { + var definitions = yamlDslSchema + .withObject("/items") + .withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + var languages = relocatedDefinitions + .withObject("/org.apache.camel.model.language.ExpressionDefinition") + .withArray("/anyOf").get(0) + .withArray("/oneOf"); + + var answer = new LinkedHashMap(); + for (var entry : languages) { + if (!"object".equals(entry.get("type").asText()) || !entry.has("required")) { + throw new Exception("Unexpected language entry " + entry.asText()); + } + var entryName = entry.withArray("/required").get(0).asText(); + var property = entry + .withObject("/properties") + .withObject("/" + entryName); + var entryDefinitionName = getNameFromRef(property); + var language = relocatedDefinitions.withObject("/" + entryDefinitionName); + if (!language.has("oneOf")) { + populateDefinitions(language, relocatedDefinitions); + answer.put(entryName, language); + continue; + } + + var langOneOf = language.withArray("/oneOf"); + if (langOneOf.size() != 2) { + throw new Exception(String.format( + "Language '%s' has '%s' entries in oneOf unexpectedly, look it closer", + entryDefinitionName, + langOneOf.size())); + } + for (var def : langOneOf) { + if (def.get("type").asText().equals("object")) { + var objectDef = (ObjectNode) def; + objectDef.set("title", language.get("title")); + objectDef.set("description", language.get("description")); + populateDefinitions(objectDef, relocatedDefinitions); + answer.put(entryName, (ObjectNode) def); + break; + } + } + } + return answer; + } + + /** + * Extract the entity definitions from the main Camel YAML DSL JSON schema in + * the usable + * format for uniforms to render the configuration form. "entity" here means the + * top level + * properties in Camel YAML DSL, such as "route", "rest", "beans", + * "routeConfiguration", etc. + * They are marked with "@YamlIn" annotation in the Camel codebase. + * It does a couple of things: + *
  • Remove "oneOf" and "anyOf"
  • + *
  • Remove properties those are supposed to be handled separately: + *
      + *
    • "steps": branching steps
    • + *
    • "parameters": component parameters
    • + *
    • expression languages
    • + *
    • dataformats
    • + *
    + *
  • + *
  • If the processor is expression aware, it puts "expression" as a + * "$comment" in the schema
  • + *
  • If the processor is dataformat aware, it puts "dataformat" as a + * "$comment" in the schema
  • + *
  • If the processor property is expression aware, it puts "expression" as a + * "$comment" in the property schema
  • + * + * @return + */ + public Map getEntities() throws Exception { + var definitions = yamlDslSchema + .withObject("/items") + .withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + var yamlIn = yamlDslSchema + .withObject("/items") + .withObject("/properties"); + + var answer = new LinkedHashMap(); + for (var yamlInRef : yamlIn.properties()) { + var yamlInName = yamlInRef.getKey(); + var yamlInRefValue = (ObjectNode) yamlInRef.getValue(); + var yamlInFQCN = getNameFromRef(yamlInRefValue); + var yamlInDefinition = relocatedDefinitions.withObject("/" + yamlInFQCN); + yamlInDefinition = extractFromOneOf(yamlInFQCN, yamlInDefinition); + yamlInDefinition.remove("oneOf"); + yamlInDefinition = extractFromAnyOfOneOf(yamlInFQCN, yamlInDefinition); + yamlInDefinition.remove("anyOf"); + Set propToRemove = new HashSet<>(); + var yamlInProperties = yamlInDefinition.withObject("/properties"); + for (var yamlInPropertyEntry : yamlInProperties.properties()) { + var propertyName = yamlInPropertyEntry.getKey(); + var property = (ObjectNode) yamlInPropertyEntry.getValue(); + var refParent = property.findParent("$ref"); + if (refParent != null) { + var ref = getNameFromRef(refParent); + if (processorReferenceBlockList.contains(ref)) { + if (yamlInDefinition.has("$comment")) { + yamlInDefinition.put("$comment", yamlInDefinition.get("$comment").asText() + ",steps"); + } else { + yamlInDefinition.put("$comment", "steps"); + } + propToRemove.add(propertyName); + } + if (EXPRESSION_SUB_ELEMENT_DEFINITION.equals(ref)) { + refParent.remove("$ref"); + refParent.put("type", "object"); + refParent.put("$comment", "expression"); + } + continue; + } + if (!property.has("type")) { + // inherited properties, such as for expression - supposed to be handled + // separately + propToRemove.add(propertyName); + } + } + propToRemove.forEach(yamlInProperties::remove); + populateDefinitions(yamlInDefinition, relocatedDefinitions); + sanitizeDefinitions(yamlInFQCN, yamlInDefinition); + answer.put(yamlInName, yamlInDefinition); + } + return answer; + } + + public Map getLoadBalancers() throws Exception { + var definitions = yamlDslSchema + .withObject("/items") + .withObject("/definitions"); + var relocatedDefinitions = relocateToRootDefinitions(definitions); + var loadBalancerAnyOfOneOf = relocatedDefinitions + .withObject("/" + LOAD_BALANCE_DEFINITION) + .withArray("/anyOf").get(0) + .withArray("/oneOf"); + + var answer = new LinkedHashMap(); + for (var entry : loadBalancerAnyOfOneOf) { + if (entry.has("not")) { + continue; + } + if (!"object".equals(entry.get("type").asText()) || !entry.has("required")) { + throw new Exception("Unexpected loadbalancer entry " + entry.asText()); + } + var entryName = entry.withArray("/required").get(0).asText(); + var property = entry + .withObject("/properties") + .withObject("/" + entryName); + var entryDefinitionName = getNameFromRef(property); + var loadBalancer = relocatedDefinitions.withObject("/" + entryDefinitionName); + if (loadBalancer.has("oneOf")) { + var lbOneOf = loadBalancer.withArray("/oneOf"); + if (lbOneOf.size() != 2) { + throw new Exception(String.format( + "LoadBalancer '%s' has '%s' entries in oneOf unexpectedly, look it closer", + entryDefinitionName, + lbOneOf.size())); + } + for (var def : lbOneOf) { + if (def.get("type").asText().equals("object")) { + var objectDef = (ObjectNode) def; + objectDef.set("title", loadBalancer.get("title")); + objectDef.set("description", loadBalancer.get("description")); + loadBalancer = objectDef; + break; + } + } + } + populateDefinitions(loadBalancer, relocatedDefinitions); + for (var prop : loadBalancer.withObject("/properties").properties()) { + var propertyDef = (ObjectNode) prop.getValue(); + var refParent = propertyDef.findParent("$ref"); + if (refParent != null) { + var ref = getNameFromRef(refParent); + if (EXPRESSION_SUB_ELEMENT_DEFINITION.equals(ref)) { + refParent.remove("$ref"); + refParent.put("type", "object"); + refParent.put("$comment", "expression"); + } + } + } + answer.put(entryName, loadBalancer); + } + return answer; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CatalogGeneratorBuilder.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CatalogGeneratorBuilder.java new file mode 100644 index 000000000..94d1d1580 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/CatalogGeneratorBuilder.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.generator; + +import static io.kaoto.camelcatalog.model.Constants.CAMEL_CATALOG_AGGREGATE; +import static io.kaoto.camelcatalog.model.Constants.CAMEL_YAML_DSL_FILE_NAME; +import static io.kaoto.camelcatalog.model.Constants.CRD_SCHEMA; +import static io.kaoto.camelcatalog.model.Constants.K8S_V1_OPENAPI; +import static io.kaoto.camelcatalog.model.Constants.KAMELETS; +import static io.kaoto.camelcatalog.model.Constants.KAMELETS_AGGREGATE; +import static io.kaoto.camelcatalog.model.Constants.KAMELET_BOUNDARIES_FILENAME; +import static io.kaoto.camelcatalog.model.Constants.KAMELET_BOUNDARIES_KEY; +import static io.kaoto.camelcatalog.model.Constants.KUBERNETES_DEFINITIONS; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.kaoto.camelcatalog.maven.CamelCatalogVersionLoader; +import io.kaoto.camelcatalog.model.CatalogDefinition; +import io.kaoto.camelcatalog.model.CatalogDefinitionEntry; +import io.kaoto.camelcatalog.model.CatalogRuntime; + +/** + * Collects the camel metadata files such as catalog and schema and + * tailors them to fit with Kaoto needs. + * This class expects the following directory structure under inputDirectory: + * + *
      + *
    • catalog/ - The root directory of extracted camel-catalog
    • + *
    • crds/ - Holds Camel K CRD YAML files
    • + *
    • kamelets/ - Holds Kamelet definition YAML files
    • + *
    • schema/ - Holds Camel YAML DSL JSON schema files
    • + *
    + * + * In addition to what is generated from above input files, this plugin + * generates index.json file that holds the list of all the generated. + */ +public class CatalogGeneratorBuilder { + + private CatalogRuntime runtime; + private String camelCatalogVersion; + private String kameletsVersion; + private String camelKCRDsVersion; + private File outputDirectory; + private boolean verbose = false; + + public CatalogGeneratorBuilder withRuntime(CatalogRuntime runtime) { + this.runtime = runtime; + return this; + } + + public CatalogGeneratorBuilder withCamelCatalogVersion(String camelCatalogVersion) { + this.camelCatalogVersion = camelCatalogVersion; + return this; + } + + public CatalogGeneratorBuilder withKameletsVersion(String kameletsVersion) { + this.kameletsVersion = kameletsVersion; + return this; + } + + public CatalogGeneratorBuilder withCamelKCRDsVersion(String camelKCRDsVersion) { + this.camelKCRDsVersion = camelKCRDsVersion; + return this; + } + + public CatalogGeneratorBuilder withOutputDirectory(File outputDirectory) { + this.outputDirectory = outputDirectory; + return this; + } + + public CatalogGeneratorBuilder withVerbose(boolean verbose) { + this.verbose = verbose; + return this; + } + + public CatalogGenerator build() { + CamelCatalogVersionLoader camelCatalogVersionLoader = new CamelCatalogVersionLoader(runtime, verbose); + var catalogGenerator = new CatalogGenerator(camelCatalogVersionLoader, runtime, outputDirectory); + catalogGenerator.camelCatalogVersion = camelCatalogVersion; + catalogGenerator.kameletsVersion = kameletsVersion; + catalogGenerator.camelKCRDsVersion = camelKCRDsVersion; + return catalogGenerator; + } + + public class CatalogGenerator { + private static final Logger LOGGER = Logger.getLogger(CatalogGenerator.class.getName()); + + private static final ObjectMapper jsonMapper = new ObjectMapper() + .configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); + private static final ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory()); + + private CamelCatalogVersionLoader camelCatalogVersionLoader; + private File outputDirectory; + private String camelCatalogVersion; + private String kameletsVersion; + private String camelKCRDsVersion; + + private CatalogGenerator(CamelCatalogVersionLoader camelCatalogVersionLoader, CatalogRuntime runtime, + File outputDirectory) { + this.camelCatalogVersionLoader = camelCatalogVersionLoader; + this.outputDirectory = outputDirectory; + } + + public CatalogDefinition generate() { + camelCatalogVersionLoader.loadCamelCatalog(camelCatalogVersion); + camelCatalogVersionLoader.loadCamelYamlDsl(); + camelCatalogVersionLoader.loadKameletBoundaries(); + camelCatalogVersionLoader.loadKamelets(kameletsVersion); + camelCatalogVersionLoader.loadKubernetesSchema(); + camelCatalogVersionLoader.loadCamelKCRDs(camelKCRDsVersion); + camelCatalogVersionLoader.loadLocalSchemas(); + + var catalogDefinition = new CatalogDefinition(); + var yamlDslSchemaProcessor = processCamelSchema(catalogDefinition); + processCatalog(yamlDslSchemaProcessor, catalogDefinition); + processKameletBoundaries(catalogDefinition); + processKamelets(catalogDefinition); + processK8sSchema(catalogDefinition); + processKameletsCRDs(catalogDefinition); + processAdditionalSchemas(catalogDefinition); + + try { + String filename = String.format("%s-%s.json", "index", + Util.generateHash(catalogDefinition.toString())); + + File indexFile = outputDirectory.toPath().resolve(filename).toFile(); + catalogDefinition + .setName("Camel " + camelCatalogVersionLoader.getRuntime() + " " + camelCatalogVersion); + catalogDefinition.setVersion(camelCatalogVersion); + catalogDefinition.setRuntime(camelCatalogVersionLoader.getRuntime()); + catalogDefinition.setFileName(indexFile.getName()); + + jsonMapper.writerWithDefaultPrettyPrinter().writeValue(indexFile, catalogDefinition); + + return catalogDefinition; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + + return null; + } + + private CamelYamlDslSchemaProcessor processCamelSchema(CatalogDefinition index) { + if (camelCatalogVersionLoader.getCamelYamlDslSchema() == null) { + LOGGER.severe("Camel YAML DSL JSON Schema is not loaded"); + return null; + } + + try { + var outputFileName = String.format("%s-%s.json", CAMEL_YAML_DSL_FILE_NAME, + Util.generateHash(camelCatalogVersionLoader.getCamelYamlDslSchema())); + var output = outputDirectory.toPath().resolve(outputFileName); + output.getParent().toFile().mkdirs(); + + Files.writeString(output, camelCatalogVersionLoader.getCamelYamlDslSchema()); + + var indexEntry = new CatalogDefinitionEntry( + CAMEL_YAML_DSL_FILE_NAME, + "Camel YAML DSL JSON schema", + camelCatalogVersion, + outputFileName); + index.getSchemas().put(CAMEL_YAML_DSL_FILE_NAME, indexEntry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + return null; + } + + try { + var yamlDslSchema = (ObjectNode) jsonMapper.readTree(camelCatalogVersionLoader.getCamelYamlDslSchema()); + + var schemaProcessor = new CamelYamlDslSchemaProcessor(jsonMapper, yamlDslSchema); + var schemaMap = schemaProcessor.processSubSchema(); + + schemaMap.forEach((name, subSchema) -> { + try { + var subSchemaFileName = String.format( + "%s-%s-%s.json", + CAMEL_YAML_DSL_FILE_NAME, + name, + Util.generateHash(subSchema)); + var subSchemaPath = outputDirectory.toPath().resolve(subSchemaFileName); + subSchemaPath.getParent().toFile().mkdirs(); + Files.writeString(subSchemaPath, subSchema); + var subSchemaIndexEntry = new CatalogDefinitionEntry( + name, + "Camel YAML DSL JSON schema: " + name, + camelCatalogVersion, + subSchemaFileName); + index.getSchemas().put(name, subSchemaIndexEntry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + }); + + return schemaProcessor; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + return null; + } + } + + private void processCatalog(CamelYamlDslSchemaProcessor schemaProcessor, CatalogDefinition index) { + var catalogProcessor = new CamelCatalogProcessor(camelCatalogVersionLoader.getCamelCatalog(), jsonMapper, + schemaProcessor, verbose); + try { + var catalogMap = catalogProcessor.processCatalog(); + catalogMap.forEach((name, catalog) -> { + try { + // Adding Kamelet & Pipe Configuration Schema to the Entities Catalog + if (name.equals("entities")) { + var catalogNode = jsonMapper.readTree(catalog); + String customSchemas[] = { "KameletConfiguration", "PipeConfiguration" }; + for (String customSchema : customSchemas) { + ((ObjectNode) catalogNode).putObject(customSchema) + .putObject("propertiesSchema"); + ((ObjectNode) catalogNode.path(customSchema).path("propertiesSchema")) + .setAll((ObjectNode) jsonMapper.readTree( + camelCatalogVersionLoader.getLocalSchemas().get(customSchema))); + } + + StringWriter writer = new StringWriter(); + var jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter(); + jsonMapper.writeTree(jsonGenerator, catalogNode); + catalog = writer.toString(); + } + + var outputFileName = String.format( + "%s-%s-%s.json", CAMEL_CATALOG_AGGREGATE, name, Util.generateHash(catalog)); + var output = outputDirectory.toPath().resolve(outputFileName); + Files.writeString(output, catalog); + var indexEntry = new CatalogDefinitionEntry( + name, + "Aggregated Camel catalog for " + name, + camelCatalogVersion, + outputFileName); + index.getCatalogs().put(name, indexEntry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + }); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + + private void processKameletBoundaries(CatalogDefinition index) { + if (camelCatalogVersionLoader.getKameletBoundaries().isEmpty()) { + LOGGER.severe("Kamelet boundaries are not loaded"); + return; + } + + var indexEntry = getKameletsEntry(camelCatalogVersionLoader.getKameletBoundaries(), KAMELET_BOUNDARIES_KEY, + KAMELET_BOUNDARIES_FILENAME, "Aggregated Kamelet boundaries in JSON"); + index.getCatalogs().put(indexEntry.name(), indexEntry); + } + + private void processKamelets(CatalogDefinition index) { + if (camelCatalogVersionLoader.getKamelets().isEmpty()) { + LOGGER.severe("Kamelets are not loaded"); + } + + var indexEntry = getKameletsEntry(camelCatalogVersionLoader.getKamelets(), KAMELETS, KAMELETS_AGGREGATE, + "Aggregated Kamelets in JSON"); + index.getCatalogs().put(indexEntry.name(), indexEntry); + } + + private CatalogDefinitionEntry getKameletsEntry(List kamelets, String name, String filename, + String description) { + var root = jsonMapper.createObjectNode(); + + try { + kamelets.forEach(kamelet -> { + processKameletFile(kamelet, root); + }); + + JsonFactory jsonFactory = new JsonFactory(); + var outputStream = new ByteArrayOutputStream(); + var writer = new OutputStreamWriter(outputStream); + + try (JsonGenerator jsonGenerator = jsonFactory.createGenerator(writer).useDefaultPrettyPrinter()) { + jsonMapper.writeTree(jsonGenerator, root); + var rootBytes = outputStream.toByteArray(); + var outputFileName = String.format("%s-%s.json", filename, Util.generateHash(rootBytes)); + var output = outputDirectory.toPath().resolve(outputFileName); + + Files.write(output, rootBytes); + + return new CatalogDefinitionEntry( + name, + description, + kameletsVersion, + outputFileName); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + + return null; + } + + private void processKameletFile(String kamelet, ObjectNode targetObject) { + try { + JsonNode kameletNode = yamlMapper.readTree(kamelet); + String lowerFileName = kameletNode.get("metadata").get("name").asText().toLowerCase(); + + KameletProcessor.process((ObjectNode) kameletNode); + targetObject.putIfAbsent(lowerFileName, kameletNode); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + + private void processK8sSchema(CatalogDefinition index) { + if (camelCatalogVersionLoader.getKubernetesSchema() == null) { + LOGGER.severe("Kubernetes JSON Schema is not loaded"); + } + + try { + var openapiSpec = (ObjectNode) jsonMapper.readTree(camelCatalogVersionLoader.getKubernetesSchema()); + var processor = new K8sSchemaProcessor(jsonMapper, openapiSpec); + var schemaMap = processor.processK8sDefinitions(KUBERNETES_DEFINITIONS); + for (var entry : schemaMap.entrySet()) { + var name = entry.getKey(); + var schema = entry.getValue(); + var outputFileName = String.format("%s-%s-%s.json", K8S_V1_OPENAPI, name, + Util.generateHash(schema)); + var output = outputDirectory.toPath().resolve(outputFileName); + Files.writeString(output, schema); + var indexEntry = new CatalogDefinitionEntry( + name, + "Kubernetes OpenAPI JSON schema: " + name, + "v1", + outputFileName); + index.getSchemas().put(name, indexEntry); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + + private void processKameletsCRDs(CatalogDefinition index) { + if (camelCatalogVersionLoader.getCamelKCRDs().isEmpty()) { + LOGGER.severe("CamelK CRDs are not loaded"); + return; + } + + camelCatalogVersionLoader.getCamelKCRDs().forEach(crdString -> { + processKameletCRD(crdString, index); + }); + } + + private void processKameletCRD(String crdString, CatalogDefinition index) { + try { + var crd = yamlMapper.readValue(crdString, CustomResourceDefinition.class); + var schema = crd.getSpec().getVersions().get(0).getSchema().getOpenAPIV3Schema(); + var name = crd.getSpec().getNames().getKind(); + + var bytes = jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(schema); + var outputFileName = String.format( + "%s-%s-%s.json", CRD_SCHEMA, name.toLowerCase(), Util.generateHash(bytes)); + + var output = outputDirectory.toPath().resolve(outputFileName); + Files.write(output, bytes); + var description = name; + + var indexEntry = new CatalogDefinitionEntry( + name, + description, + camelKCRDsVersion, + outputFileName); + index.getSchemas().put(name, indexEntry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + + private void processAdditionalSchemas(CatalogDefinition index) { + if (camelCatalogVersionLoader.getLocalSchemas().isEmpty()) { + LOGGER.severe("Local schemas are not loaded"); + return; + } + + for (var localSchemaEntry : camelCatalogVersionLoader.getLocalSchemas().entrySet()) { + try { + var schema = (ObjectNode) jsonMapper.readTree(localSchemaEntry.getValue()); + var name = localSchemaEntry.getKey(); + var description = schema.get("description").asText(); + + var outputFileName = String.format("%s-%s.%s", localSchemaEntry.getKey(), + Util.generateHash(localSchemaEntry.getValue()), "json"); + var output = outputDirectory.toPath().resolve(outputFileName); + + Files.writeString(output, localSchemaEntry.getValue()); + + var indexEntry = new CatalogDefinitionEntry( + name, + description, + "1", + outputFileName); + + index.getSchemas().put(name, indexEntry); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + } + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessor.java new file mode 100644 index 000000000..cd60f0ad8 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/K8sSchemaProcessor.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.generator; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Process Kubernetes OpenAPI specification JSON. + */ +public class K8sSchemaProcessor { + private static final Logger LOGGER = Logger.getLogger(K8sSchemaProcessor.class.getName()); + private final ObjectMapper jsonMapper; + private final ObjectNode openApiSpec; + + public K8sSchemaProcessor(ObjectMapper mapper, ObjectNode k8sOpenApiSpec) { + this.jsonMapper = mapper; + this.openApiSpec = k8sOpenApiSpec; + } + + /** + * Get k8s definitions schema from its OpenAPI spec. + * + * @param definitions + * @return + * @throws Exception + */ + public Map processK8sDefinitions(List definitions) throws Exception { + var answer = new LinkedHashMap(); + var k8sSchemas = openApiSpec.withObject("/components/schemas"); + if (definitions == null) { + return answer; + } + for (String name : definitions) { + var definition = jsonMapper.createObjectNode(); + definition.put("$schema", "http://json-schema.org/draft-07/schema#"); + definition.put("additionalProperties", false); + definition.setAll(k8sSchemas.withObject("/" + name)); + populateReferences(definition, k8sSchemas); + definition = removeKubernetesCustomKeywords(definition); + var nameSplit = name.split("\\."); + var displayName = nameSplit[nameSplit.length - 1]; + // ATM we use only few of k8s schemas, so use the short name until we see a + // conflict + var writer = new StringWriter(); + + try (JsonGenerator jsonGenerator = new JsonFactory().createGenerator(writer).useDefaultPrettyPrinter()) { + jsonMapper.writeTree(jsonGenerator, definition); + answer.put(displayName, writer.toString()); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + + } + return answer; + } + + private void populateReferences(ObjectNode definition, ObjectNode k8sSchemas) { + var added = true; + while (added) { + added = false; + for (JsonNode refParent : definition.findParents("$ref")) { + var ref = refParent.get("$ref").asText(); + if (ref.startsWith("#/components")) { + ((ObjectNode) refParent).put("$ref", ref.replace("#/components/schemas", "#/definitions")); + ref = refParent.get("$ref").asText(); + } + var name = ref.replace("#/definitions/", ""); + if (!definition.has("definitions") || !definition.withObject("/definitions").has(name)) { + var additionalDefinitions = definition.withObject("/definitions"); + additionalDefinitions.set(name, k8sSchemas.withObject("/" + name)); + added = true; + break; + } + } + } + } + + private ObjectNode removeKubernetesCustomKeywords(ObjectNode definition) { + var modified = jsonMapper.createObjectNode(); + definition.fields().forEachRemaining(node -> { + if (!node.getKey().startsWith("x-kubernetes")) { + var value = node.getValue(); + if (value.isObject()) { + value = removeKubernetesCustomKeywords((ObjectNode) value); + } else if (value.isArray()) { + value = removeKubernetesCustomKeywordsFromArrayNode((ArrayNode) value); + } + modified.set(node.getKey(), value); + } + }); + return modified; + } + + private ArrayNode removeKubernetesCustomKeywordsFromArrayNode(ArrayNode definition) { + var modified = jsonMapper.createArrayNode(); + definition.forEach(node -> { + if (node.isObject()) { + node = removeKubernetesCustomKeywords((ObjectNode) node); + } else if (node.isArray()) { + node = removeKubernetesCustomKeywordsFromArrayNode((ArrayNode) node); + } + modified.add(node); + }); + return modified; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/KameletProcessor.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/KameletProcessor.java new file mode 100644 index 000000000..a36723b91 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/KameletProcessor.java @@ -0,0 +1,39 @@ +package io.kaoto.camelcatalog.generator; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.List; + +public class KameletProcessor { + private static final List TO_STRING_TYPES = List.of("binary"); + + public static void process(ObjectNode kamelet) { + var schema = kamelet.withObject("/propertiesSchema"); + var kameletDef = kamelet.withObject("/spec") + .withObject("/definition"); + schema.put("$schema", "http://json-schema.org/draft-07/schema#"); + schema.put("type", "object"); + if (kameletDef.has("title")) schema.set("title", kameletDef.get("title")); + if (kameletDef.has("description")) schema.set("description", kameletDef.get("description")); + if (kameletDef.has("required")) schema.set("required", kameletDef.get("required")); + if (kameletDef.has("properties") && !kameletDef.withObject("/properties").isEmpty()) { + var kameletProperties = kameletDef.withObject("/properties"); + var schemaProperties = schema.withObject("/properties"); + for (var entry : kameletProperties.properties()) { + var name = entry.getKey(); + var property = entry.getValue(); + var schemaProperty = schemaProperties.withObject("/" + name); + if (property.has("type")) schemaProperty.set("type", property.get("type")); + if (TO_STRING_TYPES.contains(property.get("type").asText())) { + schemaProperty.put("$comment", "type:" + property.get("type").asText()); + schemaProperty.put("type", "string"); + } + if (property.has("title")) schemaProperty.set("title", property.get("title")); + if (property.has("description")) schemaProperty.set("description", property.get("description")); + if (property.has("enum")) schemaProperty.set("enum", property.get("enum")); + if (property.has("default")) schemaProperty.set("default", property.get("default")); + if (property.has("format")) schemaProperty.set("format", property.get("format")); + } + } + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java new file mode 100644 index 000000000..378a4d2f6 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/generator/Util.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.generator; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.io.FilenameUtils; + +public class Util { + public static String generateHash(byte[] content) throws Exception { + if (content == null) + return null; + var digest = java.security.MessageDigest.getInstance("MD5"); + var hash = digest.digest(content); + return new java.math.BigInteger(1, hash).toString(16); + } + + public static String generateHash(Path path) throws Exception { + return path == null ? null : generateHash(Files.readAllBytes(path)); + } + + public static String generateHash(String content) throws Exception { + return content == null ? null : generateHash(content.getBytes()); + } + + public static String getNormalizedFolder(String folder) { + // Get the current working directory + Path currentDirectory = Paths.get("").toAbsolutePath(); + + // Resolve the relative path + Path absolutePath = currentDirectory.resolve(folder); + String normalizedfolder = FilenameUtils.separatorsToUnix(absolutePath.toString()); + + return normalizedfolder; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/CamelCatalogVersionLoader.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/CamelCatalogVersionLoader.java new file mode 100644 index 000000000..7ef5fe7b6 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/CamelCatalogVersionLoader.java @@ -0,0 +1,314 @@ +package io.kaoto.camelcatalog.maven; + +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.catalog.DefaultRuntimeProvider; +import org.apache.camel.catalog.quarkus.QuarkusRuntimeProvider; +import org.apache.camel.springboot.catalog.SpringBootRuntimeProvider; +import org.apache.camel.tooling.model.OtherModel; + +import io.kaoto.camelcatalog.model.CatalogRuntime; +import io.kaoto.camelcatalog.model.Constants; +import io.kaoto.camelcatalog.model.MavenCoordinates; + +public class CamelCatalogVersionLoader { + private static final Logger LOGGER = Logger.getLogger(CamelCatalogVersionLoader.class.getName()); + private final KaotoMavenVersionManager KAOTO_VERSION_MANAGER = new KaotoMavenVersionManager(); + private final CamelCatalog camelCatalog = new DefaultCamelCatalog(false); + private final Map kameletBoundaries = new HashMap<>(); + private final Map kamelets = new HashMap<>(); + private final List camelKCRDs = new ArrayList<>(); + private final Map localSchemas = new HashMap<>(); + private final CatalogRuntime runtime; + private String camelYamlDSLSchema; + private String kubernetesSchema; + private boolean verbose = false; + + public CamelCatalogVersionLoader(CatalogRuntime runtime, boolean verbose) { + this.runtime = runtime; + this.verbose = verbose; + KAOTO_VERSION_MANAGER.setLog(verbose); + camelCatalog.setVersionManager(KAOTO_VERSION_MANAGER); + } + + public CatalogRuntime getRuntime() { + return runtime; + } + + public CamelCatalog getCamelCatalog() { + return camelCatalog; + } + + public String getCamelYamlDslSchema() { + return camelYamlDSLSchema; + } + + public List getKameletBoundaries() { + return kameletBoundaries.values().stream().toList(); + } + + public List getKamelets() { + return kamelets.values().stream().toList(); + } + + public String getKubernetesSchema() { + return kubernetesSchema; + } + + public List getCamelKCRDs() { + return camelKCRDs; + } + + public Map getLocalSchemas() { + return localSchemas; + } + + public boolean loadCamelCatalog(String version) { + if (version != null) { + configureRepositories(version); + MavenCoordinates mavenCoordinates = getCatalogMavenCoordinates(runtime, version); + loadDependencyInClasspath(mavenCoordinates); + } + + /** + * Check the current runtime, so we can apply the corresponding RuntimeProvider + * to the catalog + */ + switch (runtime) { + case Quarkus: + camelCatalog.setRuntimeProvider(new QuarkusRuntimeProvider()); + break; + case SpringBoot: + camelCatalog.setRuntimeProvider(new SpringBootRuntimeProvider()); + break; + default: + camelCatalog.setRuntimeProvider(new DefaultRuntimeProvider()); + break; + } + + return camelCatalog.getCatalogVersion() != null; + } + + public boolean loadCamelYamlDsl() { + OtherModel yamlDSLModel = camelCatalog.otherModel("yaml-dsl"); + MavenCoordinates mavenCoordinates = getYamlDslMavenCoordinates(runtime, yamlDSLModel); + loadDependencyInClasspath(mavenCoordinates); + + ClassLoader classLoader = KAOTO_VERSION_MANAGER.getClassLoader(); + URL resourceURL = classLoader.getResource(Constants.CAMEL_YAML_DSL_ARTIFACT); + if (resourceURL == null) { + return false; + } + + try (InputStream inputStream = resourceURL.openStream()) { + try (Scanner scanner = new Scanner(inputStream)) { + scanner.useDelimiter("\\A"); + camelYamlDSLSchema = scanner.hasNext() ? scanner.next() : ""; + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + return false; + } + + return camelYamlDSLSchema != null; + } + + public boolean loadKameletBoundaries() { + loadResourcesFromFolderAsString("kamelet-boundaries", kameletBoundaries, ".kamelet.yaml"); + return !kameletBoundaries.isEmpty(); + } + + public boolean loadKamelets(String version) { + if (version != null) { + // If the version is null, we load the installed version + MavenCoordinates mavenCoordinates = new MavenCoordinates(Constants.APACHE_CAMEL_KAMELETS_ORG, + Constants.KAMELETS_PACKAGE, + version); + loadDependencyInClasspath(mavenCoordinates); + } + + loadResourcesFromFolderAsString("kamelets", kamelets, ".kamelet.yaml"); + + return !kamelets.isEmpty(); + } + + public boolean loadKubernetesSchema() { + String url = "https://raw.githubusercontent.com/kubernetes/kubernetes/master/api/openapi-spec/v3/api__v1_openapi.json"; + + try (InputStream in = new URI(url).toURL().openStream(); + Scanner scanner = new Scanner(in, StandardCharsets.UTF_8.name())) { + scanner.useDelimiter("\\A"); + kubernetesSchema = scanner.hasNext() ? scanner.next() : ""; + } catch (IOException | URISyntaxException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + return false; + } + + return true; + } + + public boolean loadCamelKCRDs(String version) { + MavenCoordinates mavenCoordinates = new MavenCoordinates(Constants.APACHE_CAMEL_K_ORG, + Constants.CAMEL_K_CRDS_PACKAGE, + version); + boolean areCamelKCRDsLoaded = loadDependencyInClasspath(mavenCoordinates); + + ClassLoader classLoader = KAOTO_VERSION_MANAGER.getClassLoader(); + + for (String crd : Constants.CAMEL_K_CRDS_ARTIFACTS) { + URL resourceURL = classLoader.getResource(crd); + if (resourceURL == null) { + return false; + } + + try (InputStream inputStream = resourceURL.openStream()) { + try (Scanner scanner = new Scanner(inputStream)) { + scanner.useDelimiter("\\A"); + camelKCRDs.add(scanner.hasNext() ? scanner.next() : ""); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + return false; + } + } + + return areCamelKCRDsLoaded; + } + + public void loadLocalSchemas() { + loadResourcesFromFolderAsString("schemas", localSchemas, ".json"); + } + + private void configureRepositories(String version) { + if (KAOTO_VERSION_MANAGER.repositories.get("central") == null) + KAOTO_VERSION_MANAGER.addMavenRepository("central", "https://repo1.maven.org/maven2/"); + + if (version.contains("redhat") && KAOTO_VERSION_MANAGER.repositories.get("maven.redhat.ga") == null) + KAOTO_VERSION_MANAGER.addMavenRepository("maven.redhat.ga", "https://maven.repository.redhat.com/ga/"); + } + + private void loadResourcesFromFolderAsString(String resourceFolderName, Map filesMap, + String fileSuffix) { + ClassLoader classLoader = KAOTO_VERSION_MANAGER.getClassLoader(); + + try { + Iterator it = classLoader.getResources(resourceFolderName).asIterator(); + + while (it.hasNext()) { + URL resourceUrl = it.next(); + + if ("jar".equals(resourceUrl.getProtocol())) { + JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection(); + JarFile jarFile = connection.getJarFile(); + Enumeration entries = jarFile.entries(); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().startsWith(connection.getEntryName()) && !entry.isDirectory() + && entry.getName().endsWith(fileSuffix)) { + + if (verbose) { + LOGGER.log(Level.INFO, "Parsing: " + entry.getName()); + } + + try (InputStream inputStream = jarFile.getInputStream(entry)) { + try (Scanner scanner = new Scanner(inputStream)) { + scanner.useDelimiter("\\A"); + String filenameWithoutExtension = entry.getName() + .replace(resourceFolderName + "/", "") + .replace(fileSuffix, ""); + filesMap.put(filenameWithoutExtension, scanner.hasNext() ? scanner.next() : ""); + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + } + } else if ("file".equals(resourceUrl.getProtocol())) { + try { + Files.walk(Paths.get(resourceUrl.toURI())) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(fileSuffix)) + .forEach(path -> { + if (verbose) { + LOGGER.log(Level.INFO, "Parsing: " + path.toString()); + } + + try { + String filenameWithoutExtension = path.toFile().getName().substring(0, + path.toFile().getName().lastIndexOf('.')); + filesMap.put(filenameWithoutExtension, new String(Files.readAllBytes(path))); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + }); + } catch (IOException | URISyntaxException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + } + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e.toString(), e); + } + } + + private MavenCoordinates getCatalogMavenCoordinates(CatalogRuntime runtime, String version) { + switch (runtime) { + case Quarkus: + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG + ".quarkus", "camel-quarkus-catalog", version); + case SpringBoot: + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG + ".springboot", + "camel-catalog-provider-springboot", + version); + default: + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG, "camel-catalog", version); + } + } + + private MavenCoordinates getYamlDslMavenCoordinates(CatalogRuntime runtime, OtherModel yamlDSLModel) { + switch (runtime) { + case Quarkus: + String version = yamlDSLModel.getMetadata().get("camelVersion").toString(); + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG, Constants.CAMEL_YAML_DSL_PACKAGE, version); + case SpringBoot: + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG, Constants.CAMEL_YAML_DSL_PACKAGE, + yamlDSLModel.getVersion()); + default: + return new MavenCoordinates(Constants.APACHE_CAMEL_ORG, + Constants.CAMEL_YAML_DSL_PACKAGE, + yamlDSLModel.getVersion()); + } + } + + /* + * This method is used to load a dependency in the classpath. This is a + * workaround + * to load dependencies that are not in the classpath, while the Kamel Catalog + * exposes a method to load dependencies in the classpath. + */ + private boolean loadDependencyInClasspath(MavenCoordinates mavenCoordinates) { + return camelCatalog.loadRuntimeProviderVersion(mavenCoordinates.getGroupId(), mavenCoordinates.getArtifactId(), + mavenCoordinates.getVersion()); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoMavenVersionManager.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoMavenVersionManager.java new file mode 100644 index 000000000..9b3a1a026 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoMavenVersionManager.java @@ -0,0 +1,179 @@ +package io.kaoto.camelcatalog.maven; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.camel.catalog.maven.MavenVersionManager; +import org.apache.camel.tooling.maven.MavenArtifact; +import org.apache.camel.tooling.maven.MavenDownloader; +import org.apache.camel.tooling.maven.MavenDownloaderImpl; + +/** + * This class is a copy of the MavenVersionManager class from the Apache Camel + * Catalog project. + * + * This is needed because the `resolve` method doesn't resolve transitive + * dependencies + * and we need to load the underlying Camel YAML DSL from Quarkus and Spring + * Boot + * runtime providers. + */ +public class KaotoMavenVersionManager extends MavenVersionManager { + private static final Logger LOGGER = Logger.getLogger(KaotoMavenVersionManager.class.getName()); + + protected final MavenDownloader downloader; + protected final Map repositories = new LinkedHashMap<>(); + private String version; + private String runtimeProviderVersion; + private boolean log; + + private KaotoMavenVersionManager(MavenDownloaderImpl downloader) { + this.downloader = downloader; + downloader.build(); + } + + public KaotoMavenVersionManager() { + this(new MavenDownloaderImpl()); + this.setClassLoader(new KaotoOpenURLClassLoader()); + } + + public boolean getLog() { + return log; + } + + public void setLog(boolean log) { + this.log = log; + } + + /** + * To add a 3rd party Maven repository. + * + * @param name the repository name + * @param url the repository url + */ + public void addMavenRepository(String name, String url) { + super.addMavenRepository(name, url); + repositories.put(name, url); + } + + @Override + public String getLoadedVersion() { + return version; + } + + @Override + public String getRuntimeProviderLoadedVersion() { + return runtimeProviderVersion; + } + + @Override + public boolean loadRuntimeProviderVersion(String groupId, String artifactId, String version) { + try { + MavenDownloader mavenDownloader = downloader; + String gav = String.format("%s:%s:%s", groupId, artifactId, version); + + if (artifactId.contains("schema")) { + resolve(mavenDownloader, gav, version.contains("SNAPSHOT"), true); + } else { + resolve(mavenDownloader, gav, version.contains("SNAPSHOT"), false); + } + + if (artifactId.contains("catalog")) { + this.version = version; + } else { + this.runtimeProviderVersion = version; + } + + return true; + } catch (Exception e) { + if (getLog()) { + LOGGER.log(Level.WARNING, + String.format("Cannot load runtime provider version {} due {}", version, e.getMessage()), e); + } + return false; + } + } + + /** + * Resolves Maven artifact using passed coordinates and use downloaded artifact + * as one of the URLs in the + * helperClassLoader, so further Catalog access may load resources from it. + */ + public void resolve(MavenDownloader mavenDownloader, String gav, boolean useSnapshots, boolean transitive) { + try { + Set extraRepositories = new LinkedHashSet<>(repositories.values()); + + // non-transitive resolve, because we load static data from the catalog + // artifacts + List artifacts = mavenDownloader.resolveArtifacts(Collections.singletonList(gav), + extraRepositories, transitive, useSnapshots); + + if (getLog()) { + LOGGER.info("Artifacts: " + artifacts); + } + + for (MavenArtifact ma : artifacts) { + ((KaotoOpenURLClassLoader) getClassLoader()).addURL(ma.getFile().toURI().toURL()); + } + } catch (Throwable e) { + e.printStackTrace(); + } + + } + + @Override + public InputStream getResourceAsStream(String name) { + InputStream is = null; + + if (runtimeProviderVersion != null) { + is = doGetResourceAsStream(name, runtimeProviderVersion); + } + if (is == null && version != null) { + is = doGetResourceAsStream(name, version); + } + if (getClassLoader() != null && is == null) { + is = getClassLoader().getResourceAsStream(name); + } + + return is; + } + + private InputStream doGetResourceAsStream(String name, String version) { + if (version == null) { + return null; + } + + try { + URL found = null; + Enumeration urls = getClassLoader().getResources(name); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + if (url.getPath().contains(version)) { + found = url; + break; + } + } + if (found != null) { + return found.openStream(); + } + } catch (IOException e) { + if (getLog()) { + LOGGER.log(Level.WARNING, String.format("Cannot open resource {} and version {} due {}", name, version, + e.getMessage(), e)); + + } + } + + return null; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoOpenURLClassLoader.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoOpenURLClassLoader.java new file mode 100644 index 000000000..3107394ae --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/maven/KaotoOpenURLClassLoader.java @@ -0,0 +1,39 @@ +/* + * 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 io.kaoto.camelcatalog.maven; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * This class is a copy of the OpenURLClassLoader class from the Apache Camel + * Catalog project. + * + * This is a workaround because the original class is package private + */ +class KaotoOpenURLClassLoader extends URLClassLoader { + + public KaotoOpenURLClassLoader() { + super(new URL[0]); + } + + @Override + public void addURL(URL url) { + super.addURL(url); + } + +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogCliArgument.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogCliArgument.java new file mode 100644 index 000000000..29b76ffa3 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogCliArgument.java @@ -0,0 +1,60 @@ +package io.kaoto.camelcatalog.model; + +public class CatalogCliArgument { + private CatalogRuntime runtime; + private String catalogVersion; + + public CatalogCliArgument() { + } + + public CatalogCliArgument(CatalogRuntime runtime, String version) { + this.runtime = runtime; + this.catalogVersion = version; + } + + public CatalogRuntime getRuntime() { + return runtime; + } + + public void setRuntime(CatalogRuntime runtime) { + this.runtime = runtime; + } + + public String getCatalogVersion() { + return catalogVersion; + } + + public void setCatalogVersion(String version) { + this.catalogVersion = version; + } + + @Override + public String toString() { + return "CatalogCliArgument{" + + "runtime=" + runtime + + ", version='" + catalogVersion + '\'' + + '}'; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((runtime == null) ? 0 : runtime.hashCode()); + result = prime * result + ((catalogVersion == null) ? 0 : catalogVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + CatalogCliArgument other = (CatalogCliArgument) obj; + return runtime == other.runtime && catalogVersion.equals(other.catalogVersion); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinition.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinition.java new file mode 100644 index 000000000..bf8dfbbda --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinition.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed 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 io.kaoto.camelcatalog.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class CatalogDefinition { + private String name; + private String version; + private CatalogRuntime runtime; + private Map catalogs = new HashMap<>(); + private Map schemas = new HashMap<>(); + private String fileName; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public CatalogRuntime getRuntime() { + return runtime; + } + + public void setRuntime(CatalogRuntime runtime) { + this.runtime = runtime; + } + + public Map getCatalogs() { + return catalogs; + } + + public Map getSchemas() { + return schemas; + } + + @JsonIgnore + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinitionEntry.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinitionEntry.java new file mode 100644 index 000000000..6f1621983 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogDefinitionEntry.java @@ -0,0 +1,3 @@ +package io.kaoto.camelcatalog.model; + +public record CatalogDefinitionEntry(String name, String description, String version, String file) {} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibrary.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibrary.java new file mode 100644 index 000000000..978e771a8 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibrary.java @@ -0,0 +1,31 @@ +package io.kaoto.camelcatalog.model; + +import java.util.ArrayList; +import java.util.List; + +public class CatalogLibrary { + private String name; + private List definitions = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getDefinitions() { + return definitions; + } + + public void addDefinition(CatalogDefinition catalogDefinition) { + CatalogLibraryEntry entry = new CatalogLibraryEntry( + catalogDefinition.getName(), + catalogDefinition.getVersion(), + catalogDefinition.getRuntime(), + catalogDefinition.getFileName()); + + definitions.add(entry); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibraryEntry.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibraryEntry.java new file mode 100644 index 000000000..8c7e48cf1 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogLibraryEntry.java @@ -0,0 +1,3 @@ +package io.kaoto.camelcatalog.model; + +public record CatalogLibraryEntry(String name, String version, CatalogRuntime runtime, String fileName) {} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogRuntime.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogRuntime.java new file mode 100644 index 000000000..28b8cb509 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/CatalogRuntime.java @@ -0,0 +1,17 @@ +package io.kaoto.camelcatalog.model; + +public enum CatalogRuntime { + Main, + Quarkus, + SpringBoot; + + public static CatalogRuntime fromString(String name) { + for (CatalogRuntime runtime : CatalogRuntime.values()) { + if (runtime.name().equalsIgnoreCase(name)) { + return runtime; + } + } + + throw new IllegalArgumentException("No enum found with name: " + name); + } +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/Constants.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/Constants.java new file mode 100644 index 000000000..df38892e0 --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/Constants.java @@ -0,0 +1,39 @@ +package io.kaoto.camelcatalog.model; + +import java.util.List; + +public class Constants { + public static final String COMPONENTS = "components"; + public static final String DATAFORMATS = "dataformats"; + public static final String LANGUAGES = "languages"; + public static final String MODELS = "models"; + + public static final String APACHE_CAMEL_ORG = "org.apache.camel"; + public static final String APACHE_CAMEL_KAMELETS_ORG = APACHE_CAMEL_ORG + ".kamelets"; + public static final String APACHE_CAMEL_K_ORG = APACHE_CAMEL_ORG + ".k"; + public static final String CAMEL_YAML_DSL_PACKAGE = "camel-yaml-dsl"; + public static final String KAMELETS_PACKAGE = "camel-kamelets"; + public static final String CAMEL_K_CRDS_PACKAGE = "camel-k-crds"; + + public static final String CAMEL_YAML_DSL_ARTIFACT = "schema/camelYamlDsl.json"; + public static final List CAMEL_K_CRDS_ARTIFACTS = List.of( + "camel.apache.org_integrations.yaml", + "camel.apache.org_kameletbindings.yaml", + "camel.apache.org_kamelets.yaml", + "camel.apache.org_pipes.yaml"); + + public static final List KUBERNETES_DEFINITIONS = List.of( + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", + "io.k8s.api.core.v1.ObjectReference"); + + public static final String SCHEMA = "schema"; + public static final String CAMEL_YAML_DSL_FILE_NAME = "camelYamlDsl"; + public static final String K8S_V1_OPENAPI = "kubernetes-api-v1-openapi"; + public static final String CAMEL_CATALOG_AGGREGATE = "camel-catalog-aggregate"; + public static final String CRD_SCHEMA = "crd-schema"; + public static final String KAMELETS = "kamelets"; + public static final String KAMELET_BOUNDARIES_KEY = "kameletBoundaries"; + public static final String KAMELET_BOUNDARIES_FILENAME = "kamelet-boundaries"; + public static final String KAMELETS_AGGREGATE = "kamelets-aggregate"; + +} diff --git a/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/MavenCoordinates.java b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/MavenCoordinates.java new file mode 100644 index 000000000..0b58164db --- /dev/null +++ b/packages/catalog-generator/src/main/java/io/kaoto/camelcatalog/model/MavenCoordinates.java @@ -0,0 +1,25 @@ +package io.kaoto.camelcatalog.model; + +public class MavenCoordinates { + private String groupId; + private String artifactId; + private String version; + + public MavenCoordinates(String groupId, String artifactId, String version) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getVersion() { + return version; + } +} diff --git a/packages/catalog-generator/src/main/resources/kamelet-boundaries/sink.kamelet.yaml b/packages/catalog-generator/src/main/resources/kamelet-boundaries/sink.kamelet.yaml new file mode 100644 index 000000000..c49cb62ef --- /dev/null +++ b/packages/catalog-generator/src/main/resources/kamelet-boundaries/sink.kamelet.yaml @@ -0,0 +1,26 @@ +apiVersion: "camel.apache.org/v1" +kind: "Kamelet" +metadata: + name: "sink" + annotations: + camel.apache.org/kamelet.support.level: "Stable" + camel.apache.org/catalog.version: "0.0.0" + camel.apache.org/kamelet.icon: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSIxMTYuNzgxIiBjeT0iMTIyLjY4NiIgY3g9IjgwLjY3MSIgaWQ9ImQiPjxzdG9wIHN0b3AtY29sb3I9IiNmZmU5Y2EiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZjk2MDAiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJhIj48c3RvcCBzdG9wLWNvbG9yPSIjYzc3NTAwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjZmZiNTRjIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgcj0iNTMuMTUyIiBjeT0iMTM2Ljc4NCIgY3g9IjczLjQyNyIgaWQ9ImMiPjxzdG9wIHN0b3AtY29sb3I9IiNmZmYiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNjYTc3MDAiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5Mj0iNTguMDgzIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB4MT0iNjguMjUxIiBpZD0iYiI+PHN0b3Agc3RvcC1jb2xvcj0iIzUxMzAwMCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmYmE1OCIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImYiPjxzdG9wIHN0b3AtY29sb3I9IiNjNzc1MDAiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZmI1NGMiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSIxMDMuNDMiIGN5PSItMTEuMzc2IiBjeD0iLTExLjA0MyIgaWQ9ImciPjxzdG9wIHN0b3AtY29sb3I9IiNmZmYiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZmNmOGIiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSI5NS40OTgiIGN5PSIxMjAuNTg4IiBjeD0iNjkuOTQ4IiBpZD0iZSI+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmZiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmOTYwMCIgb2Zmc2V0PSIxIi8+PC9yYWRpYWxHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeGxpbms6aHJlZj0iI2EiIGlkPSJtIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjYzLjI3MSIgeTE9IjM0LjU4NiIgeDI9IjYzLjI3MSIgeTI9IjcxLjU4NCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjYiIgaWQ9Im4iIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjguMjUxIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB5Mj0iNTguMDgzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIC0zLjA0NyAtMy4xMTUpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNjIiBpZD0ibyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI3My40MjciIGN5PSIxMzYuNzg0IiByPSI1My4xNTIiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgLTMuMDQ3IC0zLjExNSkiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2QiIGlkPSJwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjgwLjY3MSIgY3k9IjEyMi42ODYiIHI9IjExNi43ODEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgLTMuMDQ3IC0zLjExNSkiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2UiIGlkPSJxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjY5Ljk0OCIgY3k9IjEyMC41ODgiIHI9Ijk1LjQ5OCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjZiIgaWQ9InIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjMuOTk5IiB5MT0iNTkuMDg0IiB4Mj0iNjMuOTk5IiB5Mj0iMTQ1LjA4MiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxyYWRpYWxHcmFkaWVudCB4bGluazpocmVmPSIjZyIgaWQ9InMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjeD0iLTExLjA0MyIgY3k9Ii0xMS4zNzYiIHI9IjEwMy40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjaCIgaWQ9InYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjguMjUxIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB5Mj0iNTguMDgzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2kiIGlkPSJ3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjczLjQyNyIgY3k9IjEzNi43ODQiIHI9IjUzLjE1MiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNqIiBpZD0ieCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI4MC42NzEiIGN5PSIxMjIuNjg2IiByPSIxMTYuNzgxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2siIGlkPSJ5IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjY5Ljk0OCIgY3k9IjEyMC41ODgiIHI9Ijk1LjQ5OCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNsIiBpZD0iQSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSItMTEuMDQzIiBjeT0iLTExLjM3NiIgcj0iMTAzLjQzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cGF0dGVybiBwYXR0ZXJuVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB3aWR0aD0iODIuNzc5IiBoZWlnaHQ9IjgyLjY0MiIgcGF0dGVyblRyYW5zZm9ybT0idHJhbnNsYXRlKDUuODM1IDMuMTE1KSIgaWQ9IkIiPjxwYXRoIGQ9Ik0zMC44NzYgMjIuM2MtMy45OTYuNzAzLTcuMjI5IDIuNjc2LTkuMzQ5IDUuNzA1bC0uMDY2LjA5NS4wMDUuMTE4Yy4wODEgMi4xOTIuNTg4IDQuNDQ5IDEuNTA0IDYuNzFsLjAzNi4wODRjLjM1NC44NjIuNzcgMS43MyAxLjI0MiAyLjU4MWwuMDU1LjA5Ny43MTYgMS4yMDQuMDY1LjEwMi44MyAxLjIyOS4wODEuMTEuNzk5IDEuMDQ4LjE4OC4yMzZhMzAuMTMxIDMwLjEzMSAwIDAgMCAyLjI0NSAyLjQ2N2MwIC4wMDIuODk1Ljg1Ljg5NS44NWwuMjMyLjIxNCAxLjE1My45ODMuMjU1LjIuOTkyLjc2Mi4zNDguMjUzIDEuMjk1Ljg5MSAxLjQ4My45MDIuMzQzLjE5OCAxLjQ4MS43ODcuMTg4LjA5IDEuMzY4LjYyOC4zMjYuMTQxIDEuNS41ODYuMjc3LjA5Ny0uMDAzLS4wMDIgMS4zNTguNDQxLjI1OC4wNzYgMS41MTMuMzkxLjMxLjA2OCAxLjQyNi4yNzcuMTI0LjAyMSAxLjUxOS4xOTguMjk5LjAyOWMxLjAwNy4wOSAyLjAxNS4xMjIgMi45OTUuMDlsLjI2LS4wMDggMS40NTYtLjEwNy4wOS0uMDEgMS4zNTgtLjIwMWMuMDA0LS4wMDIuMTk3LS4wMzYuMTk3LS4wMzZsMS4zOC0uMzEzLjE0LS4wNCAxLjI1OC0uMzk4LjExLS4wMzUgMS4yNjQtLjUyMS4xMDctLjA1Yy4wNjUtLjAzMSAxLjI2LS42NTQgMS4yNi0uNjU0bC4xNDQtLjA3NS4wMi0uMTUyYzEuNjY2LTguMDIzLTMuMDQ2LTE3Ljc1MS0xMS40Ni0yMy42Ni01LjY2Ny0zLjk4NC0xMi4xODItNS42OTUtMTcuODctNC42OTZ6bS4xMi42ODJjNS41MDgtLjk2OSAxMS44MzQuNzAyIDE3LjM1NiA0LjU4MSA3LjEgNC45ODYgMTEuNSAxMi43MzggMTEuNSAxOS43NjYgMCAxLjAzMi0uMTEgMi4wNDYtLjMwNiAzLjAzNmwtLjk5Mi41MTctLjE2NC4wNzctMS4yMTIuNDk4LS4xMDUuMDM2LTEuMjExLjM4Mi0uMTQ3LjA0Mi0xLjMyLjMtLjE5My4wMzQtMS4zMTMuMTk0YTMuNTQzIDMuNTQzIDAgMCAwLS4wNzkuMDFsLTEuNDE1LjEwMy0uMjUyLjAwOWMtLjk1Mi4wMy0xLjkzMyAwLTIuOTEyLS4wODhsLS4yOS0uMDI4LTEuNDc5LS4xOTItLjEyMy0uMDIxLTEuMzktLjI3YTM0Ljk3NSAzNC45NzUgMCAwIDAtLjMwMy0uMDY3bC0xLjQ3NC0uMzgxLS4yNTYtLjA3Ni0xLjMyNC0uNDMtLjI3LS4wOTQtMS40NjctLjU3Mi0uMzE5LS4xMzgtMS4zMzgtLjYxNS0uMTg1LS4wOS0xLjQ1LS43Ny0uMzEtLjE3Ny0xLjQ3OC0uOS0xLjI2OC0uODcxLS4zNDItLjI0OC0uOTctLjc0NS0uMjUtLjE5Ni0xLjEyOC0uOTY1LS4yMjYtLjIwNy0uODc2LS44MzEtLjI1Ni0uMjU0YTI5LjkwNCAyOS45MDQgMCAwIDEtMS45MzgtMi4xNTZsLS4xODUtLjIzMS0uNzgtMS4wMjUtLjA4LS4xMDgtLjgxLTEuMTk4YTUyLjczOCA1Mi43MzggMCAwIDAtLjA2Mi0uMWwtLjY3Mi0xLjEyNmEyNC4zNjMgMjQuMzYzIDAgMCAxLTEuMjg1LTIuNjQ0bC0uMDM1LS4wODRjLS44Ny0yLjE0NS0xLjM0Ny00LjI4Mi0xLjQzOC02LjM1NyAyLjAxMi0yLjgyIDUuMDU0LTQuNjY4IDguODIyLTUuMzN6IiBmaWxsPSJ1cmwoI20pIi8+PHBhdGggZD0iTTgwLjQyIDM2LjIwMmwtMy44Ni0uMDM0LTEuNjQzLjA2Ni4zMzMgNS45ODR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTU0LjE2MiAxNy41OWMtMTEuNDA1LTcuNDAzLTI1LjA1My02LjYyNC0zMC40ODQgMS43NC0xLjcyNCAyLjY1OC0yLjQxIDUuNzYxLTIuMTggOC45OTUgNS4wMS03Ljc5OCAxNy4wNDgtOC40MzggMjcuMTM4LTEuMzUgOC42OTQgNi4xMDQgMTMuMTM1IDE1Ljk3OCAxMS4yOTEgMjMuNzkgMi4wNDQtMS4xMTggMy43NzctMi42NTkgNS4wNS00LjYyMyA1LjQzMy04LjM2NS41ODktMjEuMTQ5LTEwLjgxNS0yOC41NTF6IiBmaWxsPSJ1cmwoI24pIi8+PHBhdGggZD0iTTEwLjc2OCA1Mi41NEw4LjUxIDYwLjU1Nmw3Ljc2OCA4Ljc4NSA3LjU3MS01LjQ4IDkuMjQzIDUuNjg1LjA5OCAxMC43NDcgOS44NzYgMS45NjYgNS42OTgtNy41NjQuMzM3LTIuMjg1IDUuMjI5LS4xNzcgNS4zMSA4LjE2MyA3LjM3NC0zLjkyNiA2LjUxNi03LjY4NS0uMDAyLS4wMDEuMTIzLS4wNjMtMy4yNzQtNi4xNCAzLjgxNSAyLjAwNiA3LjEyNi03LjQ3MyAxLjA4OC04LjAxMS03LjA2OC01LjI3Ni0uMDg5LTEuNjEgNS4wNDgtNi4xOTUtMy41ODctNy4yNzktNS45MDYtMS4yNjUtNS4yNzQtNi45NiAyLjUzMS0zLjM3Ny02LjAxMi01LjU5LTQuNTM1IDIuMjE0LTcuNTk1LTUuMTY4LS4xMDUtMy43OTgtOC4yMjktMy40OC0xLjg5OCA0Ljk1OC02Ljc1MS0xLjI2N0wyOS4yNDQuMzdsLTguMjA1IDEuMDI2LTEuMDUzIDguNS0xLjgxNi44MTEtNy4xNzMtMy41ODctNC42MjYgNS4zNy0uOTU5IDguODY1IDQuNDk2IDQuMTA0LTguMjIzLjIyMkwuMzUgMzQuNTE1bC45ODMgOS41MDggOS4wNDYuOTMgMi42ODIgNS40NTR6bTU0LjIxLTYuNGMtNS40MyA4LjM2OC0xOS4wNzcgOS4xNDgtMzAuNDgyIDEuNzQ0QzIzLjA5MiA0MC40NzkgMTguMjUgMjcuNjk3IDIzLjY4IDE5LjMzYzUuNDMtOC4zNjUgMTkuMDc4LTkuMTQ0IDMwLjQ4My0xLjc0IDExLjQwNCA3LjQwMyAxNi4yNDggMjAuMTg3IDEwLjgxNiAyOC41NXoiIGZpbGw9InVybCgjbykiLz48cGF0aCBkPSJNNzQuODMgNDMuNzMybC0uNDE3LTcuNDc4IDUuMjk4LS4yMDktMy41MzEtNy4xNjYtNS44MTYtMS4yNDUtNS4xOTMtNi44NTQgMi40OTMtMy4zMjQtNS45Mi01LjUwNS00LjQ2NSAyLjE4Mi03LjQ3OC01LjA5LS4xMDMtMy43MzgtOC4xMDMtMy40MjYtMS44NjggNC44OC02LjY0Ni0xLjI0NUwyOS40NDYuOTQzIDIxLjc2MSAxLjk4bDMuNTMgNi4xMjctNi43NTEgMy4wMTMtNy4wNjItMy41MzEtNC4yNTggNS4xOTIgNi41NDMgNC45ODYtMS43NjcgNy43OS05LjI0Mi41MTcgMS4wNCA5LjU1NyA5LjU1NC45MzQgNC43NzcgOS4yNDItNi43NSA2LjQ0IDguMjAzIDguODI2IDcuOTk3LTUuNTA0IDkuNzYzIDUuNzEyLjEwMyAxMC44MDEgMTEuMjE3IDIuMDc4IDEuNDU0LTkuODY2IDkuNjU4LS4zMTMgNS42MDggOC4yMDQgNy43OS0zLjk0NS00Ljk4NS05LjM0OCA1LjA4OC02LjMzNCA3LjQ3OCAzLjczOSAxLjAzOC03LjM3NHpNNjQuNjI3IDQ2LjAxYy01LjM0NSA4LjIzNS0xOC43ODQgOS4wMDMtMzAuMDExIDEuNzE0LTExLjIzLTcuMjktMTUuOTk5LTE5Ljg3NS0xMC42NTItMjguMTEyIDUuMzQ2LTguMjM2IDE4Ljc4NC05LjAwNCAzMC4wMTMtMS43MTUgMTEuMjI5IDcuMjkxIDE1Ljk5NyAxOS44NzYgMTAuNjUgMjguMTEzeiIgZmlsbD0idXJsKCNwKSIvPjxwYXRoIGQ9Ik03NC44MyA0My43MzJsLS40MTctNy40NzggNS4yOTgtLjIwOS0zLjUzMS03LjE2Ni01LjgxNi0xLjI0NS01LjE5My02Ljg1NCAyLjQ5My0zLjMyNC01LjkyLTUuNTA1LTQuNDY1IDIuMTgyLTcuNDc4LTUuMDktLjEwMy0zLjczOC04LjEwMy0zLjQyNi0xLjg2OCA0Ljg4LTYuNjQ2LTEuMjQ1TDI5LjQ0Ni45NDMgMjEuNzYxIDEuOThsMy41MyA2LjEyNy02Ljc1MSAzLjAxMy03LjA2Mi0zLjUzMS00LjI1OCA1LjE5MiA2LjU0MyA0Ljk4Ni0xLjc2NyA3Ljc5LTkuMjQyLjUxNyAxLjA0IDkuNTU3IDkuNTU0LjkzNCA0Ljc3NyA5LjI0Mi02Ljc1IDYuNDQgOC4yMDMgOC44MjYgNy45OTctNS41MDQgOS43NjMgNS43MTIuMTAzIDEwLjgwMSAxMS4yMTcgMi4wNzggMS40NTQtOS44NjYgOS42NTgtLjMxMyA1LjYwOCA4LjIwNCA3Ljc5LTMuOTQ1LTQuOTg1LTkuMzQ4IDUuMDg4LTYuMzM0IDcuNDc4IDMuNzM5IDEuMDM4LTcuMzc0ek02NC42MjcgNDYuMDFjLTUuMzQ1IDguMjM1LTE4Ljc4NCA5LjAwMy0zMC4wMTEgMS43MTQtMTEuMjMtNy4yOS0xNS45OTktMTkuODc1LTEwLjY1Mi0yOC4xMTIgNS4zNDYtOC4yMzYgMTguNzg0LTkuMDA0IDMwLjAxMy0xLjcxNSAxMS4yMjkgNy4yOTEgMTUuOTk3IDE5Ljg3NiAxMC42NSAyOC4xMTN6IiBmaWxsPSJ1cmwoI3EpIi8+PHBhdGggZD0iTTI5LjIuMDI0bC04LjQ3MyAxLjA2MS0xLjA2MSA4LjU3My0xLjQ4OC42NjQtNy4yNjktMy42MzQtNC44NzMgNS42NTgtLjk5IDkuMTQ3IDMuOTkxIDMuNjQyLTcuNjUyLjIwNkwwIDM0LjUwN2wxLjAxNyA5LjgzMiA5LjEzNS45MzljLjE1Mi4zMSAyLjI4MiA0LjY0MiAyLjQ4NCA1LjA1bC0yLjE3NSAyLjAyNC0yLjMzNSA4LjI5MiA4LjEgOS4xNjIgNy42MzgtNS41MjYgOC44ODMgNS40Ni4wOTkgMTAuODM4IDEwLjM2NyAyLjA2NCA1Ljg4Mi03Ljgxcy4yNDUtMS42NDcuMzA4LTIuMDg2bDQuNzQzLS4xNiA1LjM3OCA4LjI2NSA3LjcxMy00LjEwNiA2LjU1OC03LjczNy0uMDkzLjA3Ny4yNTYtLjUyNy0yLjc0My01LjE0NSAzLjA0OCAxLjYwMSA3LjM4NS03Ljc0MSAxLjEzLTguMzI2LTcuMS01LjNjLS4wMTQtLjIzOS0uMDYzLTEuMTE0LS4wNzMtMS4zMTQuMTY0LS4yMDMgNS4xMDQtNi4yNjUgNS4xMDQtNi4yNjVsLTMuNzYtNy42My01Ljk0NS0xLjI3My01LjAzOC02LjY1IDIuNTYyLTMuNDE2LTYuNDE2LTUuOTY2LTQuNTY4IDIuMjMxLTcuMjgxLTQuOTU0Yy0uMDEtLjMyMy0uMTA3LTMuODQ0LS4xMDctMy44NDRMNDEuMzkyLjg1OGwtMS45MjQgNS4wMjQtNi4zNC0xLjE5TDI5LjM5NyAwem0tLjEwNi43MTNsMy42NSA0LjU5IDcuMTYzIDEuMzQ0IDEuODc0LTQuODkzIDcuNjkxIDMuMjU0Yy4wMTIuMzk1LjEwNCAzLjc1Mi4xMDQgMy43NTJsNy45MSA1LjM4MSA0LjUwMS0yLjE5OCA1LjYwOSA1LjIxNi0yLjUwMSAzLjMzNSA1LjUxIDcuMjczIDUuODY4IDEuMjU2IDMuNDE0IDYuOTI4LTQuOTkxIDYuMTI4LjEwNSAxLjkwNiA3LjAzMyA1LjI1LTEuMDQ1IDcuNjk5LTYuODY4IDcuMTk5YTgxOC43MiA4MTguNzIgMCAwIDEtNC4zNy0yLjQxNnMzLjIwMiA2LjQxIDMuNDU2IDYuODg1bC02LjQxNCA3LjU2OGMtLjExMS4wNi02LjUyIDMuNDctNy4wMzYgMy43NDZsLTUuMjQzLTguMDYtNS43MTQuMTkyLS4zNjcgMi40ODRjLS4xMDIuMTM2LTUuMjY1IDYuOTktNS41MTIgNy4zMmwtOS4zODctMS44NjgtLjA5Ni0xMC42NTgtOS42MDYtNS45MDQtNy41MDMgNS40MjhjLS4zODgtLjQ0LTcuMTk3LTguMTQtNy40MzQtOC40MDYuMDk4LS4zNDQgMi4xMy03LjU2MiAyLjE4LTcuNzRsMi40MTEtMi4yNDItMi44OC01Ljg2LTguOTU2LS45Mi0uOTUtOS4xODIgMS4yODYtOC41MDQgOC43OTMtLjIzNy01LjAwMy00LjU2NWMuMDM3LS4zMzIuOTA2LTguMzc2LjkyOS04LjU4MS4xMy0uMTUzIDQuMDU4LTQuNzEyIDQuMzc4LTUuMDg1bDcuMDc5IDMuNTQgMi4xNDYtLjk2IDEuMDQzLTguNDI0IDcuNzQzLS45N3oiIGZpbGw9InVybCgjcikiLz48cGF0aCBkPSJNMzQuMDUgNDguNTk3Yy02LjY3OS00LjMzNC0xMS41LTEwLjg0My0xMi44OTYtMTcuNDA4LS45Ni00LjUxMi0uMjktOC43MTEgMS45MzgtMTIuMTQzIDIuMjI4LTMuNDMzIDUuNzktNS43NTUgMTAuMzAzLTYuNzE0IDYuNTY2LTEuMzk3IDE0LjQ3My4zNTcgMjEuMTUgNC42OTNhMzMuMDAxIDMzLjAwMSAwIDAgMSA2LjUwMSA1LjUyNSA1My43MzIgNTMuNzMyIDAgMCAwIDIuNjM4LTcuMzczbC0yLjEyNS0xLjk3Ni0zLjgyMyAxLjg2OGExLjA0MyAxLjA0MyAwIDAgMS0xLjA0My0uMDc1bC03LjQ3OC01LjA4OWExLjA0IDEuMDQgMCAwIDEtLjQ1NS0uODMybC0uMDg1LTMuMDctNi40OTMtMi43NDUtMS40ODMgMy44NzNjLS4xOC40Ny0uNjcuNzQ0LTEuMTY1LjY1MmwtNi42NDYtMS4yNDdhMS4wMzcgMS4wMzcgMCAwIDEtLjYyMi0uMzc1bC0zLjI2Ny00LjEwOC01LjU2Mi43NTIgMi43NTYgNC43ODNhMS4wMzggMS4wMzggMCAwIDEtLjQ3NyAxLjQ3bC02Ljc1IDMuMDEzYTEuMDQgMS4wNCAwIDAgMS0uODktLjAxOWwtNi4zMTgtMy4xNi0zLjA0OCAzLjcxNSA1LjY4NSA0LjMzMmExLjA0MiAxLjA0MiAwIDAgMSAuMzg2IDEuMDZsLTEuNzY3IDcuNzg5YTEuMDQgMS4wNCAwIDAgMS0uOTU4LjgxbC04LjE0Ny40NTYuODI5IDcuNjI0YzEuNDAxLjEzNiA4LjcxMi44NTIgOC43MTIuODUyLjM1Mi4wMzQuNjYxLjI0NC44MjQuNTU4bDQuNzc3IDkuMjQzYTEuMDQxIDEuMDQxIDAgMCAxLS4yMDYgMS4yMzJsLTYuMDA2IDUuNzMgMi43ODkgM2E1Ni41ODUgNTYuNTg1IDAgMCAwIDE5Ljc5Ni01Ljg1M2MtLjQ2NC0uMjctLjkyMy0uNTQ5LTEuMzc1LS44NDN6IiBmaWxsPSJ1cmwoI3MpIi8+PC9wYXR0ZXJuPjwvZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9InQiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwM2RjNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzRjYTRmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJ1IiB4MT0iNjMuMjcxIiB4Mj0iNjMuMjcxIiB5MT0iMzQuNTg2IiB5Mj0iNzEuNTg0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeGxpbms6aHJlZj0iI3QiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgMzYuMzQgMzkuMTI3KSIvPjxsaW5lYXJHcmFkaWVudCBpZD0iaCIgeDE9IjY4LjI1MSIgeDI9IjY4LjI1MSIgeTE9Ii0zLjQxOCIgeTI9IjU4LjA4MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwMDA1MSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzU4YjFmZiIvPjwvbGluZWFyR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJpIiBjeD0iNzMuNDI3IiBjeT0iMTM2Ljc4NCIgcj0iNTMuMTUyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA2YWNhIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9ImoiIGN4PSI4MC42NzEiIGN5PSIxMjIuNjg2IiByPSIxMTYuNzgxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjY2FmZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA5MGZmIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9ImsiIGN4PSI2OS45NDgiIGN5PSIxMjAuNTg4IiByPSI5NS40OTgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMDk1ZmYiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ieiIgeDE9IjYzLjk5OSIgeDI9IjYzLjk5OSIgeTE9IjU5LjA4NCIgeTI9IjE0NS4wODIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4bGluazpocmVmPSIjdCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IGlkPSJsIiBjeD0iLTExLjA0MyIgY3k9Ii0xMS4zNzYiIHI9IjEwMy40MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzhiY2VmZiIvPjwvcmFkaWFsR3JhZGllbnQ+PHBhdGggZD0iTTcwLjI2MyA2NC41NDNjLTMuOTk2LjcwMy03LjIzIDIuNjc1LTkuMzQ5IDUuNzA0bC0uMDY3LjA5NS4wMDYuMTE4Yy4wODEgMi4xOTIuNTg3IDQuNDUgMS41MDQgNi43MWwuMDM1LjA4NGMuMzU0Ljg2Mi43NzIgMS43MyAxLjI0MyAyLjU4MWwuMDU1LjA5Ny43MTYgMS4yMDQuMDY1LjEwMi44MyAxLjIyOS4wODEuMTEuNzk5IDEuMDQ5LjE4OC4yMzVhMzAuMTMxIDMwLjEzMSAwIDAgMCAyLjI0NSAyLjQ2N2MwIC4wMDIuODk1Ljg1Ljg5NS44NWwuMjMyLjIxNCAxLjE1My45ODMuMjU1LjIwMS45OTIuNzYxLjM0Ny4yNTMgMS4yOTUuODkxIDEuNDg0LjkwMi4zNDMuMTk4IDEuNDgxLjc4Ny4xODguMDkgMS4zNjguNjI4LjMyNi4xNDEgMS41LjU4Ni4yNzYuMDk3LS4wMDItLjAwMiAxLjM1OC40NDEuMjU4LjA3NiAxLjUxMy4zOTIuMzEuMDY4IDEuNDI2LjI3Ni4xMjQuMDIxIDEuNTE4LjE5OC4zLjAyOWMxLjAwNy4wOSAyLjAxNS4xMjIgMi45OTUuMDlsLjI2LS4wMDggMS40NTYtLjEwNy4wOS0uMDEgMS4zNTgtLjIwMWMuMDAzLS4wMDIuMTk3LS4wMzYuMTk3LS4wMzZsMS4zOC0uMzEzLjE0LS4wNCAxLjI1OC0uMzk4LjExLS4wMzUgMS4yNjMtLjUyMS4xMDctLjA1Yy4wNjYtLjAzMSAxLjI2LS42NTMgMS4yNi0uNjUzbC4xNDUtLjA3Ni4wMi0uMTUyYzEuNjY2LTguMDIzLTMuMDQ2LTE3Ljc1MS0xMS40Ni0yMy42Ni01LjY2Ny0zLjk4NC0xMi4xODItNS42OTUtMTcuODctNC42OTZ6bS4xMi42ODFjNS41MDgtLjk2OSAxMS44MzQuNzAyIDE3LjM1NiA0LjU4MSA3LjEgNC45ODYgMTEuNSAxMi43MzggMTEuNSAxOS43NjYgMCAxLjAzMi0uMTExIDIuMDQ2LS4zMDYgMy4wMzZsLS45OTIuNTE3LS4xNjQuMDc3LTEuMjEyLjQ5OC0uMTA1LjAzNi0xLjIxMS4zODItLjE0Ny4wNDItMS4zMi4zLS4xOTMuMDM0LTEuMzEzLjE5NGEzLjU0MyAzLjU0MyAwIDAgMC0uMDc5LjAxbC0xLjQxNS4xMDQtLjI1My4wMDhjLS45NTEuMDMxLTEuOTMyIDAtMi45MTEtLjA4OGwtLjI5LS4wMjctMS40NzktLjE5My0uMTIzLS4wMjEtMS4zOS0uMjdjLS4wMDMtLjAwMi0uMzAzLS4wNjctLjMwMy0uMDY3bC0xLjQ3NC0uMzgxLS4yNTYtLjA3Ni0xLjMyNS0uNDMtLjI3LS4wOTMtMS40NjYtLjU3My0uMzE5LS4xMzgtMS4zMzgtLjYxNS0uMTg1LS4wOS0xLjQ1LS43Ny0uMzEtLjE3Ny0xLjQ3OC0uOS0xLjI2OC0uODcxLS4zNDItLjI0OC0uOTctLjc0NC0uMjUtLjE5Ny0xLjEyOC0uOTY0LS4yMjYtLjIwOC0uODc2LS44MzEtLjI1Ni0uMjU0YTI5LjkwNCAyOS45MDQgMCAwIDEtMS45MzktMi4xNTZsLS4xODQtLjIzMS0uNzgtMS4wMjRjLS4wNDItLjA1OS0uMDgtLjEwOS0uMDgtLjEwOWwtLjgxLTEuMTk3YTU2LjQ4MiA1Ni40ODIgMCAwIDAtLjA2My0uMTAybC0uNjcyLTEuMTI1YTI0LjM2MyAyNC4zNjMgMCAwIDEtMS4yODUtMi42NDRMNjMgNzYuOTFjLS44Ny0yLjE0NS0xLjM0Ny00LjI4Mi0xLjQzOC02LjM1NyAyLjAxMi0yLjgyIDUuMDU0LTQuNjY4IDguODIyLTUuMzN6IiBmaWxsPSJ1cmwoI3UpIi8+PHBhdGggZD0iTTExOS44MDYgNzguNDQ0bC0zLjg1OS0uMDM0LTEuNjQzLjA2Ni4zMzMgNS45ODR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTkzLjU0OCA1OS44MzNjLTExLjQwNC03LjQwNC0yNS4wNTMtNi42MjUtMzAuNDgzIDEuNzQtMS43MjUgMi42NTctMi40MSA1Ljc2LTIuMTggOC45OTQgNS4wMS03Ljc5OCAxNy4wNDgtOC40MzggMjcuMTM3LTEuMzUgOC42OTUgNi4xMDQgMTMuMTM1IDE1Ljk3OCAxMS4yOTIgMjMuNzkgMi4wNDMtMS4xMTggMy43NzYtMi42NTggNS4wNS00LjYyMyA1LjQzMy04LjM2NS41ODktMjEuMTQ5LTEwLjgxNi0yOC41NTF6IiBmaWxsPSJ1cmwoI3YpIi8+PHBhdGggZD0iTTUwLjE1NSA5NC43ODJsLTIuMjU4IDguMDE2IDcuNzY4IDguNzg1IDcuNTctNS40NzkgOS4yNDQgNS42ODQuMDk4IDEwLjc0NyA5Ljg3NiAxLjk2NiA1LjY5OC03LjU2NC4zMzctMi4yODUgNS4yMjgtLjE3NyA1LjMxIDguMTYzIDcuMzc1LTMuOTI2IDYuNTE1LTcuNjg1LjEyMi0uMDYzLTMuMjczLTYuMTQgMy44MTQgMi4wMDUgNy4xMjYtNy40NzIgMS4wODgtOC4wMTItNy4wNjgtNS4yNzUtLjA4OC0xLjYxIDUuMDQ3LTYuMTk2LTMuNTg3LTcuMjc4LTUuOTA2LTEuMjY1LTUuMjc0LTYuOTYxIDIuNTMxLTMuMzc2LTYuMDEyLTUuNTktNC41MzUgMi4yMTMtNy41OTUtNS4xNjctLjEwNS0zLjc5OC04LjIyOC0zLjQ4MS0xLjg5OSA0Ljk1OC02Ljc1MS0xLjI2Ni0zLjY5Mi00LjY0MS04LjIwNSAxLjAyNy0xLjA1MyA4LjQ5OS0xLjgxNi44MTItNy4xNzMtMy41ODgtNC42MjYgNS4zNzEtLjk1OSA4Ljg2NSA0LjQ5NiA0LjEwMy04LjIyMy4yMjItMS4zMzUgOC44MzUuOTgzIDkuNTA4IDkuMDQ2LjkyOSAyLjY4MyA1LjQ1NXptNTQuMjEtNi4zOTljLTUuNDMgOC4zNjctMTkuMDc4IDkuMTQ3LTMwLjQ4MiAxLjc0My0xMS40MDUtNy40MDQtMTYuMjQ3LTIwLjE4Ny0xMC44MTctMjguNTU0IDUuNDMtOC4zNjUgMTkuMDc4LTkuMTQ0IDMwLjQ4Mi0xLjc0IDExLjQwNSA3LjQwMyAxNi4yNDkgMjAuMTg3IDEwLjgxNiAyOC41NTF6IiBmaWxsPSJ1cmwoI3cpIi8+PHBhdGggZD0iTTExNC4yMTYgODUuOTc0bC0uNDE2LTcuNDc3IDUuMjk4LS4yMS0zLjUzMS03LjE2NS01LjgxNi0xLjI0Ni01LjE5My02Ljg1NCAyLjQ5My0zLjMyNC01LjkyLTUuNTA1LTQuNDY1IDIuMTgyLTcuNDc5LTUuMDktLjEwMi0zLjczOC04LjEwMy0zLjQyNi0xLjg2OCA0Ljg4LTYuNjQ2LTEuMjQ1LTMuNjM1LTQuNTcxLTcuNjg2IDEuMDM4IDMuNTMgNi4xMjctNi43NSAzLjAxMy03LjA2Mi0zLjUzMS00LjI1OCA1LjE5MiA2LjU0MyA0Ljk4Ni0xLjc2NyA3Ljc5LTkuMjQzLjUxNyAxLjA0IDkuNTU3IDkuNTU0LjkzNCA0Ljc3OCA5LjI0Mi02Ljc1IDYuNDQgOC4yMDMgOC44MjYgNy45OTctNS41MDQgOS43NjMgNS43MTIuMTAzIDEwLjgwMSAxMS4yMTcgMi4wNzggMS40NTMtOS44NjYgOS42Ni0uMzEzIDUuNjA3IDguMjA0IDcuNzktMy45NDUtNC45ODYtOS4zNDggNS4wODgtNi4zMzQgNy40NzkgMy43NCAxLjAzOC03LjM3NXptLTEwLjIwMSAyLjI3OGMtNS4zNDUgOC4yMzUtMTguNzg0IDkuMDAzLTMwLjAxMiAxLjcxNC0xMS4yMy03LjI4OS0xNS45OTgtMTkuODc1LTEwLjY1MS0yOC4xMTEgNS4zNDYtOC4yMzcgMTguNzg0LTkuMDA1IDMwLjAxMi0xLjcxNSAxMS4yMyA3LjI5IDE1Ljk5NyAxOS44NzUgMTAuNjUgMjguMTEyeiIgZmlsbD0idXJsKCN4KSIvPjxwYXRoIGQ9Ik0xMTQuMjE2IDg1Ljk3NGwtLjQxNi03LjQ3NyA1LjI5OC0uMjEtMy41MzEtNy4xNjUtNS44MTYtMS4yNDYtNS4xOTMtNi44NTQgMi40OTMtMy4zMjQtNS45Mi01LjUwNS00LjQ2NSAyLjE4Mi03LjQ3OS01LjA5LS4xMDItMy43MzgtOC4xMDMtMy40MjYtMS44NjggNC44OC02LjY0Ni0xLjI0NS0zLjYzNS00LjU3MS03LjY4NiAxLjAzOCAzLjUzIDYuMTI3LTYuNzUgMy4wMTMtNy4wNjItMy41MzEtNC4yNTggNS4xOTIgNi41NDMgNC45ODYtMS43NjcgNy43OS05LjI0My41MTcgMS4wNCA5LjU1NyA5LjU1NC45MzQgNC43NzggOS4yNDItNi43NSA2LjQ0IDguMjAzIDguODI2IDcuOTk3LTUuNTA0IDkuNzYzIDUuNzEyLjEwMyAxMC44MDEgMTEuMjE3IDIuMDc4IDEuNDUzLTkuODY2IDkuNjYtLjMxMyA1LjYwNyA4LjIwNCA3Ljc5LTMuOTQ1LTQuOTg2LTkuMzQ4IDUuMDg4LTYuMzM0IDcuNDc5IDMuNzQgMS4wMzgtNy4zNzV6bS0xMC4yMDEgMi4yNzhjLTUuMzQ1IDguMjM1LTE4Ljc4NCA5LjAwMy0zMC4wMTIgMS43MTQtMTEuMjMtNy4yODktMTUuOTk4LTE5Ljg3NS0xMC42NTEtMjguMTExIDUuMzQ2LTguMjM3IDE4Ljc4NC05LjAwNSAzMC4wMTItMS43MTUgMTEuMjMgNy4yOSAxNS45OTcgMTkuODc1IDEwLjY1IDI4LjExMnoiIGZpbGw9InVybCgjeSkiLz48cGF0aCBkPSJNNjguNTg4IDQyLjI2NmwtOC40NzQgMS4wNjEtMS4wNjEgOC41NzMtMS40ODguNjY1LTcuMjctMy42MzUtNC44NzIgNS42NTgtLjk5IDkuMTQ3IDMuOTkxIDMuNjQyLTcuNjUyLjIwNi0xLjM4NSA5LjE2NiAxLjAxNyA5LjgzMiA5LjEzNC45MzkgMi40ODUgNS4wNTEtMi4xNzYgMi4wMjMtMi4zMzUgOC4yOTMgOC4xMDIgOS4xNjEgNy42MzctNS41MjYgOC44ODMgNS40Ni4wOTkgMTAuODM4IDEwLjM2NyAyLjA2NSA1Ljg4Mi03LjgxLjMwOC0yLjA4NyA0Ljc0My0uMTYgNS4zNzggOC4yNjUgNy43MTMtNC4xMDYgNi41NTgtNy43MzctLjA5My4wNzcuMjU2LS41MjctMi43NDMtNS4xNDUgMy4wNDggMS42MDIgNy4zODQtNy43NDIgMS4xMzEtOC4zMjYtNy4xMDEtNS4zLS4wNzItMS4zMTQgNS4xMDQtNi4yNjUtMy43Ni03LjYzLTUuOTQ1LTEuMjczLTUuMDM4LTYuNjUgMi41NjItMy40MTYtNi40MTYtNS45NjUtNC41NjkgMi4yMy03LjI4LTQuOTU0Yy0uMDEtLjMyMi0uMTA3LTMuODQ0LS4xMDctMy44NDRMODAuNzc5IDQzLjFzLTEuNzQgNC41NC0xLjkyNCA1LjAyNGMtLjUxMi0uMDk2LTYuMTA2LTEuMTQ1LTYuMzQtMS4xOWwtMy43MzItNC42OTJ6bS0uMTA3LjcxNGwzLjY1IDQuNTg5IDcuMTYzIDEuMzQ0IDEuODc0LTQuODkzIDcuNjkxIDMuMjU1Yy4wMTIuMzk1LjEwNCAzLjc1Mi4xMDQgMy43NTJsNy45MSA1LjM4IDQuNTAxLTIuMTk4IDUuNjA5IDUuMjE2LTIuNTAxIDMuMzM1IDUuNTEgNy4yNzMgNS44NjggMS4yNTdjLjEzOC4yOCAzLjIzIDYuNTU3IDMuNDE0IDYuOTI3bC00Ljk5MSA2LjEyOC4xMDUgMS45MDYgNy4wMzMgNS4yNS0xLjA0NiA3LjY5OS02Ljg2NyA3LjJjLS40MS0uMjE1LTQuMzctMi40MTYtNC4zNy0yLjQxNnMzLjIwMiA2LjQxIDMuNDU1IDYuODg0bC02LjQxNCA3LjU2OGMtLjExLjA2LTYuNTIgMy40Ny03LjAzNSAzLjc0NmwtNS4yNDQtOC4wNi01LjcxMy4xOTItLjM2NyAyLjQ4NS01LjUxMyA3LjMyLTkuMzg2LTEuODY5LS4wOTctMTAuNjU3LTkuNjA1LTUuOTA1LTcuNTAzIDUuNDI4LTcuNDM0LTguNDA2Yy4wOTgtLjM0NCAyLjEzLTcuNTYyIDIuMTgtNy43NGwyLjQxMS0yLjI0Mi0yLjg4LTUuODYtOC45NTctLjkyLS45NS05LjE4MiAxLjI4Ny04LjUwNCA4Ljc5Mi0uMjM3LTUuMDAyLTQuNTY1Yy4wMzctLjMzMi45MDYtOC4zNzYuOTI5LTguNTgxLjEzLS4xNTMgNC4wNTctNC43MTIgNC4zNzgtNS4wODVsNy4wNzkgMy41NCAyLjE0Ni0uOTU5IDEuMDQzLTguNDI1IDcuNzQzLS45N3oiIGZpbGw9InVybCgjeikiLz48cGF0aCBkPSJNNzMuNDM2IDkwLjg0Yy02LjY3OC00LjMzNC0xMS41LTEwLjg0NC0xMi44OTUtMTcuNDA4LS45Ni00LjUxMy0uMjktOC43MTIgMS45MzctMTIuMTQ0IDIuMjI4LTMuNDMzIDUuNzkxLTUuNzU1IDEwLjMwNC02LjcxNCA2LjU2Ni0xLjM5NyAxNC40NzIuMzU3IDIxLjE1IDQuNjkzYTMzLjAwMSAzMy4wMDEgMCAwIDEgNi41MDEgNS41MjUgNTMuNzMyIDUzLjczMiAwIDAgMCAyLjYzOC03LjM3M2wtMi4xMjUtMS45NzYtMy44MjMgMS44NjhhMS4wNDMgMS4wNDMgMCAwIDEtMS4wNDMtLjA3NGwtNy40NzktNS4wOWExLjA0IDEuMDQgMCAwIDEtLjQ1NC0uODMybC0uMDg1LTMuMDctNi40OTMtMi43NDUtMS40ODMgMy44NzNjLS4xOC40Ny0uNjcuNzQ0LTEuMTY1LjY1MmwtNi42NDYtMS4yNDdhMS4wMzcgMS4wMzcgMCAwIDEtLjYyMy0uMzc1bC0zLjI2Ni00LjEwOC01LjU2Mi43NTJhMjMxNzYuNSAyMzE3Ni41IDAgMCAwIDIuNzU2IDQuNzgzIDEuMDM4IDEuMDM4IDAgMCAxLS40NzcgMS40N2wtNi43NSAzLjAxNGExLjA0IDEuMDQgMCAwIDEtLjg5LS4wMmwtNi4zMTktMy4xNTktMy4wNDcgMy43MTUgNS42ODUgNC4zMzFhMS4wNDIgMS4wNDIgMCAwIDEgLjM4NSAxLjA2TDUyLjQgNjguMDNhMS4wNCAxLjA0IDAgMCAxLS45NTcuODFsLTguMTQ3LjQ1Ni44MjkgNy42MjQgOC43MTEuODUyYy4zNTIuMDM0LjY2Mi4yNDQuODI1LjU1OGw0Ljc3NyA5LjI0M2ExLjA0MSAxLjA0MSAwIDAgMS0uMjA2IDEuMjMybC02LjAwNiA1LjczIDIuNzg4IDNhNTYuNTg1IDU2LjU4NSAwIDAgMCAxOS43OTctNS44NTNjLS40NjUtLjI3LS45MjMtLjU0OS0xLjM3NS0uODQzeiIgZmlsbD0idXJsKCNBKSIvPjxwYXRoIGQ9Ik0xMjUuMjEyIDEyOEgzNi4zMzlWMzkuMTI3aDg4Ljg3M3oiIGZpbGw9Im5vbmUiLz48cGF0aCBmaWxsPSJ1cmwoI0IpIiBkPSJNNS44MzUgMy4xMTVoODIuNzc5djgyLjY0Mkg1LjgzNXoiLz48cGF0aCBkPSJNOTEuNjYgODguODczSDIuNzg5VjBoODguODczeiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==" + camel.apache.org/provider: "Kaoto" + camel.apache.org/kamelet.group: "Sink" + camel.apache.org/kamelet.namespace: "" + labels: + camel.apache.org/kamelet.type: "sink" +spec: + definition: + title: "Kamelet Sink" + description: "All source kamelets must end with this sink step." + required: + type: "object" + properties: + dependencies: + - "camel:core" + template: + from: + uri: "kamelet:sink" + steps: diff --git a/packages/catalog-generator/src/main/resources/kamelet-boundaries/source.kamelet.yaml b/packages/catalog-generator/src/main/resources/kamelet-boundaries/source.kamelet.yaml new file mode 100644 index 000000000..974fff6ac --- /dev/null +++ b/packages/catalog-generator/src/main/resources/kamelet-boundaries/source.kamelet.yaml @@ -0,0 +1,26 @@ +apiVersion: "camel.apache.org/v1" +kind: "Kamelet" +metadata: + name: "source" + annotations: + camel.apache.org/kamelet.support.level: "Stable" + camel.apache.org/catalog.version: "0.0.0" + camel.apache.org/kamelet.icon: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSIxMTYuNzgxIiBjeT0iMTIyLjY4NiIgY3g9IjgwLjY3MSIgaWQ9ImQiPjxzdG9wIHN0b3AtY29sb3I9IiNmZmU5Y2EiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZjk2MDAiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJhIj48c3RvcCBzdG9wLWNvbG9yPSIjYzc3NTAwIiBvZmZzZXQ9IjAiLz48c3RvcCBzdG9wLWNvbG9yPSIjZmZiNTRjIiBvZmZzZXQ9IjEiLz48L2xpbmVhckdyYWRpZW50PjxyYWRpYWxHcmFkaWVudCBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgcj0iNTMuMTUyIiBjeT0iMTM2Ljc4NCIgY3g9IjczLjQyNyIgaWQ9ImMiPjxzdG9wIHN0b3AtY29sb3I9IiNmZmYiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNjYTc3MDAiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB5Mj0iNTguMDgzIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB4MT0iNjguMjUxIiBpZD0iYiI+PHN0b3Agc3RvcC1jb2xvcj0iIzUxMzAwMCIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmYmE1OCIgb2Zmc2V0PSIxIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImYiPjxzdG9wIHN0b3AtY29sb3I9IiNjNzc1MDAiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZmI1NGMiIG9mZnNldD0iMSIvPjwvbGluZWFyR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSIxMDMuNDMiIGN5PSItMTEuMzc2IiBjeD0iLTExLjA0MyIgaWQ9ImciPjxzdG9wIHN0b3AtY29sb3I9IiNmZmYiIG9mZnNldD0iMCIvPjxzdG9wIHN0b3AtY29sb3I9IiNmZmNmOGIiIG9mZnNldD0iMSIvPjwvcmFkaWFsR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiByPSI5NS40OTgiIGN5PSIxMjAuNTg4IiBjeD0iNjkuOTQ4IiBpZD0iZSI+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmZiIgb2Zmc2V0PSIwIi8+PHN0b3Agc3RvcC1jb2xvcj0iI2ZmOTYwMCIgb2Zmc2V0PSIxIi8+PC9yYWRpYWxHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeGxpbms6aHJlZj0iI2EiIGlkPSJtIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjYzLjI3MSIgeTE9IjM0LjU4NiIgeDI9IjYzLjI3MSIgeTI9IjcxLjU4NCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjYiIgaWQ9Im4iIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjguMjUxIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB5Mj0iNTguMDgzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIC0zLjA0NyAtMy4xMTUpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNjIiBpZD0ibyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI3My40MjciIGN5PSIxMzYuNzg0IiByPSI1My4xNTIiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgLTMuMDQ3IC0zLjExNSkiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2QiIGlkPSJwIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjgwLjY3MSIgY3k9IjEyMi42ODYiIHI9IjExNi43ODEiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgLTMuMDQ3IC0zLjExNSkiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2UiIGlkPSJxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjY5Ljk0OCIgY3k9IjEyMC41ODgiIHI9Ijk1LjQ5OCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjZiIgaWQ9InIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjMuOTk5IiB5MT0iNTkuMDg0IiB4Mj0iNjMuOTk5IiB5Mj0iMTQ1LjA4MiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxyYWRpYWxHcmFkaWVudCB4bGluazpocmVmPSIjZyIgaWQ9InMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBjeD0iLTExLjA0MyIgY3k9Ii0xMS4zNzYiIHI9IjEwMy40MyIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAtMy4wNDcgLTMuMTE1KSIvPjxsaW5lYXJHcmFkaWVudCB4bGluazpocmVmPSIjaCIgaWQ9InYiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iNjguMjUxIiB5MT0iLTMuNDE4IiB4Mj0iNjguMjUxIiB5Mj0iNTguMDgzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2kiIGlkPSJ3IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjczLjQyNyIgY3k9IjEzNi43ODQiIHI9IjUzLjE1MiIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNqIiBpZD0ieCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI4MC42NzEiIGN5PSIxMjIuNjg2IiByPSIxMTYuNzgxIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cmFkaWFsR3JhZGllbnQgeGxpbms6aHJlZj0iI2siIGlkPSJ5IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjY5Ljk0OCIgY3k9IjEyMC41ODgiIHI9Ijk1LjQ5OCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IHhsaW5rOmhyZWY9IiNsIiBpZD0iQSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSItMTEuMDQzIiBjeT0iLTExLjM3NiIgcj0iMTAzLjQzIiBncmFkaWVudFRyYW5zZm9ybT0ibWF0cml4KC42OTQzMiAwIDAgLjY5NDMyIDM2LjM0IDM5LjEyNykiLz48cGF0dGVybiBwYXR0ZXJuVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB3aWR0aD0iODIuNzc5IiBoZWlnaHQ9IjgyLjY0MiIgcGF0dGVyblRyYW5zZm9ybT0idHJhbnNsYXRlKDUuODM1IDMuMTE1KSIgaWQ9IkIiPjxwYXRoIGQ9Ik0zMC44NzYgMjIuM2MtMy45OTYuNzAzLTcuMjI5IDIuNjc2LTkuMzQ5IDUuNzA1bC0uMDY2LjA5NS4wMDUuMTE4Yy4wODEgMi4xOTIuNTg4IDQuNDQ5IDEuNTA0IDYuNzFsLjAzNi4wODRjLjM1NC44NjIuNzcgMS43MyAxLjI0MiAyLjU4MWwuMDU1LjA5Ny43MTYgMS4yMDQuMDY1LjEwMi44MyAxLjIyOS4wODEuMTEuNzk5IDEuMDQ4LjE4OC4yMzZhMzAuMTMxIDMwLjEzMSAwIDAgMCAyLjI0NSAyLjQ2N2MwIC4wMDIuODk1Ljg1Ljg5NS44NWwuMjMyLjIxNCAxLjE1My45ODMuMjU1LjIuOTkyLjc2Mi4zNDguMjUzIDEuMjk1Ljg5MSAxLjQ4My45MDIuMzQzLjE5OCAxLjQ4MS43ODcuMTg4LjA5IDEuMzY4LjYyOC4zMjYuMTQxIDEuNS41ODYuMjc3LjA5Ny0uMDAzLS4wMDIgMS4zNTguNDQxLjI1OC4wNzYgMS41MTMuMzkxLjMxLjA2OCAxLjQyNi4yNzcuMTI0LjAyMSAxLjUxOS4xOTguMjk5LjAyOWMxLjAwNy4wOSAyLjAxNS4xMjIgMi45OTUuMDlsLjI2LS4wMDggMS40NTYtLjEwNy4wOS0uMDEgMS4zNTgtLjIwMWMuMDA0LS4wMDIuMTk3LS4wMzYuMTk3LS4wMzZsMS4zOC0uMzEzLjE0LS4wNCAxLjI1OC0uMzk4LjExLS4wMzUgMS4yNjQtLjUyMS4xMDctLjA1Yy4wNjUtLjAzMSAxLjI2LS42NTQgMS4yNi0uNjU0bC4xNDQtLjA3NS4wMi0uMTUyYzEuNjY2LTguMDIzLTMuMDQ2LTE3Ljc1MS0xMS40Ni0yMy42Ni01LjY2Ny0zLjk4NC0xMi4xODItNS42OTUtMTcuODctNC42OTZ6bS4xMi42ODJjNS41MDgtLjk2OSAxMS44MzQuNzAyIDE3LjM1NiA0LjU4MSA3LjEgNC45ODYgMTEuNSAxMi43MzggMTEuNSAxOS43NjYgMCAxLjAzMi0uMTEgMi4wNDYtLjMwNiAzLjAzNmwtLjk5Mi41MTctLjE2NC4wNzctMS4yMTIuNDk4LS4xMDUuMDM2LTEuMjExLjM4Mi0uMTQ3LjA0Mi0xLjMyLjMtLjE5My4wMzQtMS4zMTMuMTk0YTMuNTQzIDMuNTQzIDAgMCAwLS4wNzkuMDFsLTEuNDE1LjEwMy0uMjUyLjAwOWMtLjk1Mi4wMy0xLjkzMyAwLTIuOTEyLS4wODhsLS4yOS0uMDI4LTEuNDc5LS4xOTItLjEyMy0uMDIxLTEuMzktLjI3YTM0Ljk3NSAzNC45NzUgMCAwIDAtLjMwMy0uMDY3bC0xLjQ3NC0uMzgxLS4yNTYtLjA3Ni0xLjMyNC0uNDMtLjI3LS4wOTQtMS40NjctLjU3Mi0uMzE5LS4xMzgtMS4zMzgtLjYxNS0uMTg1LS4wOS0xLjQ1LS43Ny0uMzEtLjE3Ny0xLjQ3OC0uOS0xLjI2OC0uODcxLS4zNDItLjI0OC0uOTctLjc0NS0uMjUtLjE5Ni0xLjEyOC0uOTY1LS4yMjYtLjIwNy0uODc2LS44MzEtLjI1Ni0uMjU0YTI5LjkwNCAyOS45MDQgMCAwIDEtMS45MzgtMi4xNTZsLS4xODUtLjIzMS0uNzgtMS4wMjUtLjA4LS4xMDgtLjgxLTEuMTk4YTUyLjczOCA1Mi43MzggMCAwIDAtLjA2Mi0uMWwtLjY3Mi0xLjEyNmEyNC4zNjMgMjQuMzYzIDAgMCAxLTEuMjg1LTIuNjQ0bC0uMDM1LS4wODRjLS44Ny0yLjE0NS0xLjM0Ny00LjI4Mi0xLjQzOC02LjM1NyAyLjAxMi0yLjgyIDUuMDU0LTQuNjY4IDguODIyLTUuMzN6IiBmaWxsPSJ1cmwoI20pIi8+PHBhdGggZD0iTTgwLjQyIDM2LjIwMmwtMy44Ni0uMDM0LTEuNjQzLjA2Ni4zMzMgNS45ODR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTU0LjE2MiAxNy41OWMtMTEuNDA1LTcuNDAzLTI1LjA1My02LjYyNC0zMC40ODQgMS43NC0xLjcyNCAyLjY1OC0yLjQxIDUuNzYxLTIuMTggOC45OTUgNS4wMS03Ljc5OCAxNy4wNDgtOC40MzggMjcuMTM4LTEuMzUgOC42OTQgNi4xMDQgMTMuMTM1IDE1Ljk3OCAxMS4yOTEgMjMuNzkgMi4wNDQtMS4xMTggMy43NzctMi42NTkgNS4wNS00LjYyMyA1LjQzMy04LjM2NS41ODktMjEuMTQ5LTEwLjgxNS0yOC41NTF6IiBmaWxsPSJ1cmwoI24pIi8+PHBhdGggZD0iTTEwLjc2OCA1Mi41NEw4LjUxIDYwLjU1Nmw3Ljc2OCA4Ljc4NSA3LjU3MS01LjQ4IDkuMjQzIDUuNjg1LjA5OCAxMC43NDcgOS44NzYgMS45NjYgNS42OTgtNy41NjQuMzM3LTIuMjg1IDUuMjI5LS4xNzcgNS4zMSA4LjE2MyA3LjM3NC0zLjkyNiA2LjUxNi03LjY4NS0uMDAyLS4wMDEuMTIzLS4wNjMtMy4yNzQtNi4xNCAzLjgxNSAyLjAwNiA3LjEyNi03LjQ3MyAxLjA4OC04LjAxMS03LjA2OC01LjI3Ni0uMDg5LTEuNjEgNS4wNDgtNi4xOTUtMy41ODctNy4yNzktNS45MDYtMS4yNjUtNS4yNzQtNi45NiAyLjUzMS0zLjM3Ny02LjAxMi01LjU5LTQuNTM1IDIuMjE0LTcuNTk1LTUuMTY4LS4xMDUtMy43OTgtOC4yMjktMy40OC0xLjg5OCA0Ljk1OC02Ljc1MS0xLjI2N0wyOS4yNDQuMzdsLTguMjA1IDEuMDI2LTEuMDUzIDguNS0xLjgxNi44MTEtNy4xNzMtMy41ODctNC42MjYgNS4zNy0uOTU5IDguODY1IDQuNDk2IDQuMTA0LTguMjIzLjIyMkwuMzUgMzQuNTE1bC45ODMgOS41MDggOS4wNDYuOTMgMi42ODIgNS40NTR6bTU0LjIxLTYuNGMtNS40MyA4LjM2OC0xOS4wNzcgOS4xNDgtMzAuNDgyIDEuNzQ0QzIzLjA5MiA0MC40NzkgMTguMjUgMjcuNjk3IDIzLjY4IDE5LjMzYzUuNDMtOC4zNjUgMTkuMDc4LTkuMTQ0IDMwLjQ4My0xLjc0IDExLjQwNCA3LjQwMyAxNi4yNDggMjAuMTg3IDEwLjgxNiAyOC41NXoiIGZpbGw9InVybCgjbykiLz48cGF0aCBkPSJNNzQuODMgNDMuNzMybC0uNDE3LTcuNDc4IDUuMjk4LS4yMDktMy41MzEtNy4xNjYtNS44MTYtMS4yNDUtNS4xOTMtNi44NTQgMi40OTMtMy4zMjQtNS45Mi01LjUwNS00LjQ2NSAyLjE4Mi03LjQ3OC01LjA5LS4xMDMtMy43MzgtOC4xMDMtMy40MjYtMS44NjggNC44OC02LjY0Ni0xLjI0NUwyOS40NDYuOTQzIDIxLjc2MSAxLjk4bDMuNTMgNi4xMjctNi43NTEgMy4wMTMtNy4wNjItMy41MzEtNC4yNTggNS4xOTIgNi41NDMgNC45ODYtMS43NjcgNy43OS05LjI0Mi41MTcgMS4wNCA5LjU1NyA5LjU1NC45MzQgNC43NzcgOS4yNDItNi43NSA2LjQ0IDguMjAzIDguODI2IDcuOTk3LTUuNTA0IDkuNzYzIDUuNzEyLjEwMyAxMC44MDEgMTEuMjE3IDIuMDc4IDEuNDU0LTkuODY2IDkuNjU4LS4zMTMgNS42MDggOC4yMDQgNy43OS0zLjk0NS00Ljk4NS05LjM0OCA1LjA4OC02LjMzNCA3LjQ3OCAzLjczOSAxLjAzOC03LjM3NHpNNjQuNjI3IDQ2LjAxYy01LjM0NSA4LjIzNS0xOC43ODQgOS4wMDMtMzAuMDExIDEuNzE0LTExLjIzLTcuMjktMTUuOTk5LTE5Ljg3NS0xMC42NTItMjguMTEyIDUuMzQ2LTguMjM2IDE4Ljc4NC05LjAwNCAzMC4wMTMtMS43MTUgMTEuMjI5IDcuMjkxIDE1Ljk5NyAxOS44NzYgMTAuNjUgMjguMTEzeiIgZmlsbD0idXJsKCNwKSIvPjxwYXRoIGQ9Ik03NC44MyA0My43MzJsLS40MTctNy40NzggNS4yOTgtLjIwOS0zLjUzMS03LjE2Ni01LjgxNi0xLjI0NS01LjE5My02Ljg1NCAyLjQ5My0zLjMyNC01LjkyLTUuNTA1LTQuNDY1IDIuMTgyLTcuNDc4LTUuMDktLjEwMy0zLjczOC04LjEwMy0zLjQyNi0xLjg2OCA0Ljg4LTYuNjQ2LTEuMjQ1TDI5LjQ0Ni45NDMgMjEuNzYxIDEuOThsMy41MyA2LjEyNy02Ljc1MSAzLjAxMy03LjA2Mi0zLjUzMS00LjI1OCA1LjE5MiA2LjU0MyA0Ljk4Ni0xLjc2NyA3Ljc5LTkuMjQyLjUxNyAxLjA0IDkuNTU3IDkuNTU0LjkzNCA0Ljc3NyA5LjI0Mi02Ljc1IDYuNDQgOC4yMDMgOC44MjYgNy45OTctNS41MDQgOS43NjMgNS43MTIuMTAzIDEwLjgwMSAxMS4yMTcgMi4wNzggMS40NTQtOS44NjYgOS42NTgtLjMxMyA1LjYwOCA4LjIwNCA3Ljc5LTMuOTQ1LTQuOTg1LTkuMzQ4IDUuMDg4LTYuMzM0IDcuNDc4IDMuNzM5IDEuMDM4LTcuMzc0ek02NC42MjcgNDYuMDFjLTUuMzQ1IDguMjM1LTE4Ljc4NCA5LjAwMy0zMC4wMTEgMS43MTQtMTEuMjMtNy4yOS0xNS45OTktMTkuODc1LTEwLjY1Mi0yOC4xMTIgNS4zNDYtOC4yMzYgMTguNzg0LTkuMDA0IDMwLjAxMy0xLjcxNSAxMS4yMjkgNy4yOTEgMTUuOTk3IDE5Ljg3NiAxMC42NSAyOC4xMTN6IiBmaWxsPSJ1cmwoI3EpIi8+PHBhdGggZD0iTTI5LjIuMDI0bC04LjQ3MyAxLjA2MS0xLjA2MSA4LjU3My0xLjQ4OC42NjQtNy4yNjktMy42MzQtNC44NzMgNS42NTgtLjk5IDkuMTQ3IDMuOTkxIDMuNjQyLTcuNjUyLjIwNkwwIDM0LjUwN2wxLjAxNyA5LjgzMiA5LjEzNS45MzljLjE1Mi4zMSAyLjI4MiA0LjY0MiAyLjQ4NCA1LjA1bC0yLjE3NSAyLjAyNC0yLjMzNSA4LjI5MiA4LjEgOS4xNjIgNy42MzgtNS41MjYgOC44ODMgNS40Ni4wOTkgMTAuODM4IDEwLjM2NyAyLjA2NCA1Ljg4Mi03Ljgxcy4yNDUtMS42NDcuMzA4LTIuMDg2bDQuNzQzLS4xNiA1LjM3OCA4LjI2NSA3LjcxMy00LjEwNiA2LjU1OC03LjczNy0uMDkzLjA3Ny4yNTYtLjUyNy0yLjc0My01LjE0NSAzLjA0OCAxLjYwMSA3LjM4NS03Ljc0MSAxLjEzLTguMzI2LTcuMS01LjNjLS4wMTQtLjIzOS0uMDYzLTEuMTE0LS4wNzMtMS4zMTQuMTY0LS4yMDMgNS4xMDQtNi4yNjUgNS4xMDQtNi4yNjVsLTMuNzYtNy42My01Ljk0NS0xLjI3My01LjAzOC02LjY1IDIuNTYyLTMuNDE2LTYuNDE2LTUuOTY2LTQuNTY4IDIuMjMxLTcuMjgxLTQuOTU0Yy0uMDEtLjMyMy0uMTA3LTMuODQ0LS4xMDctMy44NDRMNDEuMzkyLjg1OGwtMS45MjQgNS4wMjQtNi4zNC0xLjE5TDI5LjM5NyAwem0tLjEwNi43MTNsMy42NSA0LjU5IDcuMTYzIDEuMzQ0IDEuODc0LTQuODkzIDcuNjkxIDMuMjU0Yy4wMTIuMzk1LjEwNCAzLjc1Mi4xMDQgMy43NTJsNy45MSA1LjM4MSA0LjUwMS0yLjE5OCA1LjYwOSA1LjIxNi0yLjUwMSAzLjMzNSA1LjUxIDcuMjczIDUuODY4IDEuMjU2IDMuNDE0IDYuOTI4LTQuOTkxIDYuMTI4LjEwNSAxLjkwNiA3LjAzMyA1LjI1LTEuMDQ1IDcuNjk5LTYuODY4IDcuMTk5YTgxOC43MiA4MTguNzIgMCAwIDEtNC4zNy0yLjQxNnMzLjIwMiA2LjQxIDMuNDU2IDYuODg1bC02LjQxNCA3LjU2OGMtLjExMS4wNi02LjUyIDMuNDctNy4wMzYgMy43NDZsLTUuMjQzLTguMDYtNS43MTQuMTkyLS4zNjcgMi40ODRjLS4xMDIuMTM2LTUuMjY1IDYuOTktNS41MTIgNy4zMmwtOS4zODctMS44NjgtLjA5Ni0xMC42NTgtOS42MDYtNS45MDQtNy41MDMgNS40MjhjLS4zODgtLjQ0LTcuMTk3LTguMTQtNy40MzQtOC40MDYuMDk4LS4zNDQgMi4xMy03LjU2MiAyLjE4LTcuNzRsMi40MTEtMi4yNDItMi44OC01Ljg2LTguOTU2LS45Mi0uOTUtOS4xODIgMS4yODYtOC41MDQgOC43OTMtLjIzNy01LjAwMy00LjU2NWMuMDM3LS4zMzIuOTA2LTguMzc2LjkyOS04LjU4MS4xMy0uMTUzIDQuMDU4LTQuNzEyIDQuMzc4LTUuMDg1bDcuMDc5IDMuNTQgMi4xNDYtLjk2IDEuMDQzLTguNDI0IDcuNzQzLS45N3oiIGZpbGw9InVybCgjcikiLz48cGF0aCBkPSJNMzQuMDUgNDguNTk3Yy02LjY3OS00LjMzNC0xMS41LTEwLjg0My0xMi44OTYtMTcuNDA4LS45Ni00LjUxMi0uMjktOC43MTEgMS45MzgtMTIuMTQzIDIuMjI4LTMuNDMzIDUuNzktNS43NTUgMTAuMzAzLTYuNzE0IDYuNTY2LTEuMzk3IDE0LjQ3My4zNTcgMjEuMTUgNC42OTNhMzMuMDAxIDMzLjAwMSAwIDAgMSA2LjUwMSA1LjUyNSA1My43MzIgNTMuNzMyIDAgMCAwIDIuNjM4LTcuMzczbC0yLjEyNS0xLjk3Ni0zLjgyMyAxLjg2OGExLjA0MyAxLjA0MyAwIDAgMS0xLjA0My0uMDc1bC03LjQ3OC01LjA4OWExLjA0IDEuMDQgMCAwIDEtLjQ1NS0uODMybC0uMDg1LTMuMDctNi40OTMtMi43NDUtMS40ODMgMy44NzNjLS4xOC40Ny0uNjcuNzQ0LTEuMTY1LjY1MmwtNi42NDYtMS4yNDdhMS4wMzcgMS4wMzcgMCAwIDEtLjYyMi0uMzc1bC0zLjI2Ny00LjEwOC01LjU2Mi43NTIgMi43NTYgNC43ODNhMS4wMzggMS4wMzggMCAwIDEtLjQ3NyAxLjQ3bC02Ljc1IDMuMDEzYTEuMDQgMS4wNCAwIDAgMS0uODktLjAxOWwtNi4zMTgtMy4xNi0zLjA0OCAzLjcxNSA1LjY4NSA0LjMzMmExLjA0MiAxLjA0MiAwIDAgMSAuMzg2IDEuMDZsLTEuNzY3IDcuNzg5YTEuMDQgMS4wNCAwIDAgMS0uOTU4LjgxbC04LjE0Ny40NTYuODI5IDcuNjI0YzEuNDAxLjEzNiA4LjcxMi44NTIgOC43MTIuODUyLjM1Mi4wMzQuNjYxLjI0NC44MjQuNTU4bDQuNzc3IDkuMjQzYTEuMDQxIDEuMDQxIDAgMCAxLS4yMDYgMS4yMzJsLTYuMDA2IDUuNzMgMi43ODkgM2E1Ni41ODUgNTYuNTg1IDAgMCAwIDE5Ljc5Ni01Ljg1M2MtLjQ2NC0uMjctLjkyMy0uNTQ5LTEuMzc1LS44NDN6IiBmaWxsPSJ1cmwoI3MpIi8+PC9wYXR0ZXJuPjwvZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9InQiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwM2RjNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzRjYTRmZiIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJ1IiB4MT0iNjMuMjcxIiB4Mj0iNjMuMjcxIiB5MT0iMzQuNTg2IiB5Mj0iNzEuNTg0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeGxpbms6aHJlZj0iI3QiIGdyYWRpZW50VHJhbnNmb3JtPSJtYXRyaXgoLjY5NDMyIDAgMCAuNjk0MzIgMzYuMzQgMzkuMTI3KSIvPjxsaW5lYXJHcmFkaWVudCBpZD0iaCIgeDE9IjY4LjI1MSIgeDI9IjY4LjI1MSIgeTE9Ii0zLjQxOCIgeTI9IjU4LjA4MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwMDA1MSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzU4YjFmZiIvPjwvbGluZWFyR3JhZGllbnQ+PHJhZGlhbEdyYWRpZW50IGlkPSJpIiBjeD0iNzMuNDI3IiBjeT0iMTM2Ljc4NCIgcj0iNTMuMTUyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA2YWNhIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9ImoiIGN4PSI4MC42NzEiIGN5PSIxMjIuNjg2IiByPSIxMTYuNzgxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjY2FmZmZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDA5MGZmIi8+PC9yYWRpYWxHcmFkaWVudD48cmFkaWFsR3JhZGllbnQgaWQ9ImsiIGN4PSI2OS45NDgiIGN5PSIxMjAuNTg4IiByPSI5NS40OTgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMDk1ZmYiLz48L3JhZGlhbEdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0ieiIgeDE9IjYzLjk5OSIgeDI9IjYzLjk5OSIgeTE9IjU5LjA4NCIgeTI9IjE0NS4wODIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4bGluazpocmVmPSIjdCIgZ3JhZGllbnRUcmFuc2Zvcm09Im1hdHJpeCguNjk0MzIgMCAwIC42OTQzMiAzNi4zNCAzOS4xMjcpIi8+PHJhZGlhbEdyYWRpZW50IGlkPSJsIiBjeD0iLTExLjA0MyIgY3k9Ii0xMS4zNzYiIHI9IjEwMy40MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzhiY2VmZiIvPjwvcmFkaWFsR3JhZGllbnQ+PHBhdGggZD0iTTcwLjI2MyA2NC41NDNjLTMuOTk2LjcwMy03LjIzIDIuNjc1LTkuMzQ5IDUuNzA0bC0uMDY3LjA5NS4wMDYuMTE4Yy4wODEgMi4xOTIuNTg3IDQuNDUgMS41MDQgNi43MWwuMDM1LjA4NGMuMzU0Ljg2Mi43NzIgMS43MyAxLjI0MyAyLjU4MWwuMDU1LjA5Ny43MTYgMS4yMDQuMDY1LjEwMi44MyAxLjIyOS4wODEuMTEuNzk5IDEuMDQ5LjE4OC4yMzVhMzAuMTMxIDMwLjEzMSAwIDAgMCAyLjI0NSAyLjQ2N2MwIC4wMDIuODk1Ljg1Ljg5NS44NWwuMjMyLjIxNCAxLjE1My45ODMuMjU1LjIwMS45OTIuNzYxLjM0Ny4yNTMgMS4yOTUuODkxIDEuNDg0LjkwMi4zNDMuMTk4IDEuNDgxLjc4Ny4xODguMDkgMS4zNjguNjI4LjMyNi4xNDEgMS41LjU4Ni4yNzYuMDk3LS4wMDItLjAwMiAxLjM1OC40NDEuMjU4LjA3NiAxLjUxMy4zOTIuMzEuMDY4IDEuNDI2LjI3Ni4xMjQuMDIxIDEuNTE4LjE5OC4zLjAyOWMxLjAwNy4wOSAyLjAxNS4xMjIgMi45OTUuMDlsLjI2LS4wMDggMS40NTYtLjEwNy4wOS0uMDEgMS4zNTgtLjIwMWMuMDAzLS4wMDIuMTk3LS4wMzYuMTk3LS4wMzZsMS4zOC0uMzEzLjE0LS4wNCAxLjI1OC0uMzk4LjExLS4wMzUgMS4yNjMtLjUyMS4xMDctLjA1Yy4wNjYtLjAzMSAxLjI2LS42NTMgMS4yNi0uNjUzbC4xNDUtLjA3Ni4wMi0uMTUyYzEuNjY2LTguMDIzLTMuMDQ2LTE3Ljc1MS0xMS40Ni0yMy42Ni01LjY2Ny0zLjk4NC0xMi4xODItNS42OTUtMTcuODctNC42OTZ6bS4xMi42ODFjNS41MDgtLjk2OSAxMS44MzQuNzAyIDE3LjM1NiA0LjU4MSA3LjEgNC45ODYgMTEuNSAxMi43MzggMTEuNSAxOS43NjYgMCAxLjAzMi0uMTExIDIuMDQ2LS4zMDYgMy4wMzZsLS45OTIuNTE3LS4xNjQuMDc3LTEuMjEyLjQ5OC0uMTA1LjAzNi0xLjIxMS4zODItLjE0Ny4wNDItMS4zMi4zLS4xOTMuMDM0LTEuMzEzLjE5NGEzLjU0MyAzLjU0MyAwIDAgMC0uMDc5LjAxbC0xLjQxNS4xMDQtLjI1My4wMDhjLS45NTEuMDMxLTEuOTMyIDAtMi45MTEtLjA4OGwtLjI5LS4wMjctMS40NzktLjE5My0uMTIzLS4wMjEtMS4zOS0uMjdjLS4wMDMtLjAwMi0uMzAzLS4wNjctLjMwMy0uMDY3bC0xLjQ3NC0uMzgxLS4yNTYtLjA3Ni0xLjMyNS0uNDMtLjI3LS4wOTMtMS40NjYtLjU3My0uMzE5LS4xMzgtMS4zMzgtLjYxNS0uMTg1LS4wOS0xLjQ1LS43Ny0uMzEtLjE3Ny0xLjQ3OC0uOS0xLjI2OC0uODcxLS4zNDItLjI0OC0uOTctLjc0NC0uMjUtLjE5Ny0xLjEyOC0uOTY0LS4yMjYtLjIwOC0uODc2LS44MzEtLjI1Ni0uMjU0YTI5LjkwNCAyOS45MDQgMCAwIDEtMS45MzktMi4xNTZsLS4xODQtLjIzMS0uNzgtMS4wMjRjLS4wNDItLjA1OS0uMDgtLjEwOS0uMDgtLjEwOWwtLjgxLTEuMTk3YTU2LjQ4MiA1Ni40ODIgMCAwIDAtLjA2My0uMTAybC0uNjcyLTEuMTI1YTI0LjM2MyAyNC4zNjMgMCAwIDEtMS4yODUtMi42NDRMNjMgNzYuOTFjLS44Ny0yLjE0NS0xLjM0Ny00LjI4Mi0xLjQzOC02LjM1NyAyLjAxMi0yLjgyIDUuMDU0LTQuNjY4IDguODIyLTUuMzN6IiBmaWxsPSJ1cmwoI3UpIi8+PHBhdGggZD0iTTExOS44MDYgNzguNDQ0bC0zLjg1OS0uMDM0LTEuNjQzLjA2Ni4zMzMgNS45ODR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTkzLjU0OCA1OS44MzNjLTExLjQwNC03LjQwNC0yNS4wNTMtNi42MjUtMzAuNDgzIDEuNzQtMS43MjUgMi42NTctMi40MSA1Ljc2LTIuMTggOC45OTQgNS4wMS03Ljc5OCAxNy4wNDgtOC40MzggMjcuMTM3LTEuMzUgOC42OTUgNi4xMDQgMTMuMTM1IDE1Ljk3OCAxMS4yOTIgMjMuNzkgMi4wNDMtMS4xMTggMy43NzYtMi42NTggNS4wNS00LjYyMyA1LjQzMy04LjM2NS41ODktMjEuMTQ5LTEwLjgxNi0yOC41NTF6IiBmaWxsPSJ1cmwoI3YpIi8+PHBhdGggZD0iTTUwLjE1NSA5NC43ODJsLTIuMjU4IDguMDE2IDcuNzY4IDguNzg1IDcuNTctNS40NzkgOS4yNDQgNS42ODQuMDk4IDEwLjc0NyA5Ljg3NiAxLjk2NiA1LjY5OC03LjU2NC4zMzctMi4yODUgNS4yMjgtLjE3NyA1LjMxIDguMTYzIDcuMzc1LTMuOTI2IDYuNTE1LTcuNjg1LjEyMi0uMDYzLTMuMjczLTYuMTQgMy44MTQgMi4wMDUgNy4xMjYtNy40NzIgMS4wODgtOC4wMTItNy4wNjgtNS4yNzUtLjA4OC0xLjYxIDUuMDQ3LTYuMTk2LTMuNTg3LTcuMjc4LTUuOTA2LTEuMjY1LTUuMjc0LTYuOTYxIDIuNTMxLTMuMzc2LTYuMDEyLTUuNTktNC41MzUgMi4yMTMtNy41OTUtNS4xNjctLjEwNS0zLjc5OC04LjIyOC0zLjQ4MS0xLjg5OSA0Ljk1OC02Ljc1MS0xLjI2Ni0zLjY5Mi00LjY0MS04LjIwNSAxLjAyNy0xLjA1MyA4LjQ5OS0xLjgxNi44MTItNy4xNzMtMy41ODgtNC42MjYgNS4zNzEtLjk1OSA4Ljg2NSA0LjQ5NiA0LjEwMy04LjIyMy4yMjItMS4zMzUgOC44MzUuOTgzIDkuNTA4IDkuMDQ2LjkyOSAyLjY4MyA1LjQ1NXptNTQuMjEtNi4zOTljLTUuNDMgOC4zNjctMTkuMDc4IDkuMTQ3LTMwLjQ4MiAxLjc0My0xMS40MDUtNy40MDQtMTYuMjQ3LTIwLjE4Ny0xMC44MTctMjguNTU0IDUuNDMtOC4zNjUgMTkuMDc4LTkuMTQ0IDMwLjQ4Mi0xLjc0IDExLjQwNSA3LjQwMyAxNi4yNDkgMjAuMTg3IDEwLjgxNiAyOC41NTF6IiBmaWxsPSJ1cmwoI3cpIi8+PHBhdGggZD0iTTExNC4yMTYgODUuOTc0bC0uNDE2LTcuNDc3IDUuMjk4LS4yMS0zLjUzMS03LjE2NS01LjgxNi0xLjI0Ni01LjE5My02Ljg1NCAyLjQ5My0zLjMyNC01LjkyLTUuNTA1LTQuNDY1IDIuMTgyLTcuNDc5LTUuMDktLjEwMi0zLjczOC04LjEwMy0zLjQyNi0xLjg2OCA0Ljg4LTYuNjQ2LTEuMjQ1LTMuNjM1LTQuNTcxLTcuNjg2IDEuMDM4IDMuNTMgNi4xMjctNi43NSAzLjAxMy03LjA2Mi0zLjUzMS00LjI1OCA1LjE5MiA2LjU0MyA0Ljk4Ni0xLjc2NyA3Ljc5LTkuMjQzLjUxNyAxLjA0IDkuNTU3IDkuNTU0LjkzNCA0Ljc3OCA5LjI0Mi02Ljc1IDYuNDQgOC4yMDMgOC44MjYgNy45OTctNS41MDQgOS43NjMgNS43MTIuMTAzIDEwLjgwMSAxMS4yMTcgMi4wNzggMS40NTMtOS44NjYgOS42Ni0uMzEzIDUuNjA3IDguMjA0IDcuNzktMy45NDUtNC45ODYtOS4zNDggNS4wODgtNi4zMzQgNy40NzkgMy43NCAxLjAzOC03LjM3NXptLTEwLjIwMSAyLjI3OGMtNS4zNDUgOC4yMzUtMTguNzg0IDkuMDAzLTMwLjAxMiAxLjcxNC0xMS4yMy03LjI4OS0xNS45OTgtMTkuODc1LTEwLjY1MS0yOC4xMTEgNS4zNDYtOC4yMzcgMTguNzg0LTkuMDA1IDMwLjAxMi0xLjcxNSAxMS4yMyA3LjI5IDE1Ljk5NyAxOS44NzUgMTAuNjUgMjguMTEyeiIgZmlsbD0idXJsKCN4KSIvPjxwYXRoIGQ9Ik0xMTQuMjE2IDg1Ljk3NGwtLjQxNi03LjQ3NyA1LjI5OC0uMjEtMy41MzEtNy4xNjUtNS44MTYtMS4yNDYtNS4xOTMtNi44NTQgMi40OTMtMy4zMjQtNS45Mi01LjUwNS00LjQ2NSAyLjE4Mi03LjQ3OS01LjA5LS4xMDItMy43MzgtOC4xMDMtMy40MjYtMS44NjggNC44OC02LjY0Ni0xLjI0NS0zLjYzNS00LjU3MS03LjY4NiAxLjAzOCAzLjUzIDYuMTI3LTYuNzUgMy4wMTMtNy4wNjItMy41MzEtNC4yNTggNS4xOTIgNi41NDMgNC45ODYtMS43NjcgNy43OS05LjI0My41MTcgMS4wNCA5LjU1NyA5LjU1NC45MzQgNC43NzggOS4yNDItNi43NSA2LjQ0IDguMjAzIDguODI2IDcuOTk3LTUuNTA0IDkuNzYzIDUuNzEyLjEwMyAxMC44MDEgMTEuMjE3IDIuMDc4IDEuNDUzLTkuODY2IDkuNjYtLjMxMyA1LjYwNyA4LjIwNCA3Ljc5LTMuOTQ1LTQuOTg2LTkuMzQ4IDUuMDg4LTYuMzM0IDcuNDc5IDMuNzQgMS4wMzgtNy4zNzV6bS0xMC4yMDEgMi4yNzhjLTUuMzQ1IDguMjM1LTE4Ljc4NCA5LjAwMy0zMC4wMTIgMS43MTQtMTEuMjMtNy4yODktMTUuOTk4LTE5Ljg3NS0xMC42NTEtMjguMTExIDUuMzQ2LTguMjM3IDE4Ljc4NC05LjAwNSAzMC4wMTItMS43MTUgMTEuMjMgNy4yOSAxNS45OTcgMTkuODc1IDEwLjY1IDI4LjExMnoiIGZpbGw9InVybCgjeSkiLz48cGF0aCBkPSJNNjguNTg4IDQyLjI2NmwtOC40NzQgMS4wNjEtMS4wNjEgOC41NzMtMS40ODguNjY1LTcuMjctMy42MzUtNC44NzIgNS42NTgtLjk5IDkuMTQ3IDMuOTkxIDMuNjQyLTcuNjUyLjIwNi0xLjM4NSA5LjE2NiAxLjAxNyA5LjgzMiA5LjEzNC45MzkgMi40ODUgNS4wNTEtMi4xNzYgMi4wMjMtMi4zMzUgOC4yOTMgOC4xMDIgOS4xNjEgNy42MzctNS41MjYgOC44ODMgNS40Ni4wOTkgMTAuODM4IDEwLjM2NyAyLjA2NSA1Ljg4Mi03LjgxLjMwOC0yLjA4NyA0Ljc0My0uMTYgNS4zNzggOC4yNjUgNy43MTMtNC4xMDYgNi41NTgtNy43MzctLjA5My4wNzcuMjU2LS41MjctMi43NDMtNS4xNDUgMy4wNDggMS42MDIgNy4zODQtNy43NDIgMS4xMzEtOC4zMjYtNy4xMDEtNS4zLS4wNzItMS4zMTQgNS4xMDQtNi4yNjUtMy43Ni03LjYzLTUuOTQ1LTEuMjczLTUuMDM4LTYuNjUgMi41NjItMy40MTYtNi40MTYtNS45NjUtNC41NjkgMi4yMy03LjI4LTQuOTU0Yy0uMDEtLjMyMi0uMTA3LTMuODQ0LS4xMDctMy44NDRMODAuNzc5IDQzLjFzLTEuNzQgNC41NC0xLjkyNCA1LjAyNGMtLjUxMi0uMDk2LTYuMTA2LTEuMTQ1LTYuMzQtMS4xOWwtMy43MzItNC42OTJ6bS0uMTA3LjcxNGwzLjY1IDQuNTg5IDcuMTYzIDEuMzQ0IDEuODc0LTQuODkzIDcuNjkxIDMuMjU1Yy4wMTIuMzk1LjEwNCAzLjc1Mi4xMDQgMy43NTJsNy45MSA1LjM4IDQuNTAxLTIuMTk4IDUuNjA5IDUuMjE2LTIuNTAxIDMuMzM1IDUuNTEgNy4yNzMgNS44NjggMS4yNTdjLjEzOC4yOCAzLjIzIDYuNTU3IDMuNDE0IDYuOTI3bC00Ljk5MSA2LjEyOC4xMDUgMS45MDYgNy4wMzMgNS4yNS0xLjA0NiA3LjY5OS02Ljg2NyA3LjJjLS40MS0uMjE1LTQuMzctMi40MTYtNC4zNy0yLjQxNnMzLjIwMiA2LjQxIDMuNDU1IDYuODg0bC02LjQxNCA3LjU2OGMtLjExLjA2LTYuNTIgMy40Ny03LjAzNSAzLjc0NmwtNS4yNDQtOC4wNi01LjcxMy4xOTItLjM2NyAyLjQ4NS01LjUxMyA3LjMyLTkuMzg2LTEuODY5LS4wOTctMTAuNjU3LTkuNjA1LTUuOTA1LTcuNTAzIDUuNDI4LTcuNDM0LTguNDA2Yy4wOTgtLjM0NCAyLjEzLTcuNTYyIDIuMTgtNy43NGwyLjQxMS0yLjI0Mi0yLjg4LTUuODYtOC45NTctLjkyLS45NS05LjE4MiAxLjI4Ny04LjUwNCA4Ljc5Mi0uMjM3LTUuMDAyLTQuNTY1Yy4wMzctLjMzMi45MDYtOC4zNzYuOTI5LTguNTgxLjEzLS4xNTMgNC4wNTctNC43MTIgNC4zNzgtNS4wODVsNy4wNzkgMy41NCAyLjE0Ni0uOTU5IDEuMDQzLTguNDI1IDcuNzQzLS45N3oiIGZpbGw9InVybCgjeikiLz48cGF0aCBkPSJNNzMuNDM2IDkwLjg0Yy02LjY3OC00LjMzNC0xMS41LTEwLjg0NC0xMi44OTUtMTcuNDA4LS45Ni00LjUxMy0uMjktOC43MTIgMS45MzctMTIuMTQ0IDIuMjI4LTMuNDMzIDUuNzkxLTUuNzU1IDEwLjMwNC02LjcxNCA2LjU2Ni0xLjM5NyAxNC40NzIuMzU3IDIxLjE1IDQuNjkzYTMzLjAwMSAzMy4wMDEgMCAwIDEgNi41MDEgNS41MjUgNTMuNzMyIDUzLjczMiAwIDAgMCAyLjYzOC03LjM3M2wtMi4xMjUtMS45NzYtMy44MjMgMS44NjhhMS4wNDMgMS4wNDMgMCAwIDEtMS4wNDMtLjA3NGwtNy40NzktNS4wOWExLjA0IDEuMDQgMCAwIDEtLjQ1NC0uODMybC0uMDg1LTMuMDctNi40OTMtMi43NDUtMS40ODMgMy44NzNjLS4xOC40Ny0uNjcuNzQ0LTEuMTY1LjY1MmwtNi42NDYtMS4yNDdhMS4wMzcgMS4wMzcgMCAwIDEtLjYyMy0uMzc1bC0zLjI2Ni00LjEwOC01LjU2Mi43NTJhMjMxNzYuNSAyMzE3Ni41IDAgMCAwIDIuNzU2IDQuNzgzIDEuMDM4IDEuMDM4IDAgMCAxLS40NzcgMS40N2wtNi43NSAzLjAxNGExLjA0IDEuMDQgMCAwIDEtLjg5LS4wMmwtNi4zMTktMy4xNTktMy4wNDcgMy43MTUgNS42ODUgNC4zMzFhMS4wNDIgMS4wNDIgMCAwIDEgLjM4NSAxLjA2TDUyLjQgNjguMDNhMS4wNCAxLjA0IDAgMCAxLS45NTcuODFsLTguMTQ3LjQ1Ni44MjkgNy42MjQgOC43MTEuODUyYy4zNTIuMDM0LjY2Mi4yNDQuODI1LjU1OGw0Ljc3NyA5LjI0M2ExLjA0MSAxLjA0MSAwIDAgMS0uMjA2IDEuMjMybC02LjAwNiA1LjczIDIuNzg4IDNhNTYuNTg1IDU2LjU4NSAwIDAgMCAxOS43OTctNS44NTNjLS40NjUtLjI3LS45MjMtLjU0OS0xLjM3NS0uODQzeiIgZmlsbD0idXJsKCNBKSIvPjxwYXRoIGQ9Ik0xMjUuMjEyIDEyOEgzNi4zMzlWMzkuMTI3aDg4Ljg3M3oiIGZpbGw9Im5vbmUiLz48cGF0aCBmaWxsPSJ1cmwoI0IpIiBkPSJNNS44MzUgMy4xMTVoODIuNzc5djgyLjY0Mkg1LjgzNXoiLz48cGF0aCBkPSJNOTEuNjYgODguODczSDIuNzg5VjBoODguODczeiIgZmlsbD0ibm9uZSIvPjwvc3ZnPg==" + camel.apache.org/provider: "Kaoto" + camel.apache.org/kamelet.group: "Source" + camel.apache.org/kamelet.namespace: "" + labels: + camel.apache.org/kamelet.type: "source" +spec: + definition: + title: "Kamelet Source" + description: "All sink kamelets must start with this source step." + required: + type: "object" + properties: + dependencies: + - "camel:core" + template: + from: + uri: "kamelet:source" + steps: diff --git a/packages/catalog-generator/src/main/resources/schemas/KameletConfiguration.json b/packages/catalog-generator/src/main/resources/schemas/KameletConfiguration.json new file mode 100644 index 000000000..57e990dba --- /dev/null +++ b/packages/catalog-generator/src/main/resources/schemas/KameletConfiguration.json @@ -0,0 +1,139 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "title": "Kamelet configuration", + "description": "Schema for Kamelet configuration", + "properties": { + "name": { + "title": "Name", + "description": "Name of the kamelet", + "type": "string" + }, + "title": { + "title": "Title", + "description": "Title of the kamelet", + "type": "string" + }, + "description": { + "title": "Description", + "description": "Formal description of the kamelet", + "type": "string" + }, + "type": { + "title": "Kamelet Type", + "description": "Select the Kamelet type from the available options", + "type": "string", + "enum": [ + "source", + "action", + "sink" + ] + }, + "icon": { + "title": "Kamelet Icon", + "description": "Choose icon for the kamelet", + "type": "string" + }, + "supportLevel": { + "title": "Support Level", + "description": "Support Level of the kamelet", + "type": "string" + }, + "catalogVersion": { + "title": "Catalog Version", + "description": "Catalog Version of the kamelet", + "type": "string" + }, + "provider": { + "title": "Provider", + "description": "Provider of the kamelet", + "type": "string" + }, + "group": { + "title": "Group", + "description": "Group of the kamelet", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "description": "Namespace of the kamelet", + "type": "string" + }, + "labels": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "title": "Additional Labels", + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object" + }, + "annotations": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "title": "Additional Annotations", + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object" + }, + "kameletProperties": { + "title": "Properties", + "type": "array", + "description": "Configure properties on the Kamelet", + "items": { + "type": "object", + "properties": { + "name": { + "title": "Property name", + "description": "Name of the property", + "type": "string" + }, + "title": { + "title": "Title", + "description": "Display name of the property", + "type": "string" + }, + "description": { + "title": "Description", + "description": "Simple text description of the property", + "type": "string" + }, + "type": { + "title": "Property type", + "description": "Set the expected type for this property", + "type": "string", + "enum": [ + "string", + "number", + "boolean" + ], + "default": "string" + }, + "default": { + "title": "Default", + "description": "Default value for the property", + "type": "string" + }, + "x-descriptors": { + "title": "X-descriptors", + "description": "Specific aids for the visual tools", + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name", + "type" + ] + } + } + }, + "required": [ + "name", + "type" + ] +} diff --git a/packages/catalog-generator/src/main/resources/schemas/PipeConfiguration.json b/packages/catalog-generator/src/main/resources/schemas/PipeConfiguration.json new file mode 100644 index 000000000..4392c6728 --- /dev/null +++ b/packages/catalog-generator/src/main/resources/schemas/PipeConfiguration.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "title": "Pipe configuration", + "description": "Schema for Pipe configuration", + "properties": { + "name": { + "title": "Name", + "description": "Name of the Pipe", + "type": "string" + }, + "labels": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "title": "Labels", + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object" + }, + "annotations": { + "additionalProperties": { + "default": "", + "type": "string" + }, + "title": "Annotations", + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object" + } + }, + "required": [ + "name" + ] +} diff --git a/packages/catalog-generator/src/main/resources/schemas/PipeErrorHandler.json b/packages/catalog-generator/src/main/resources/schemas/PipeErrorHandler.json new file mode 100644 index 000000000..9f538476e --- /dev/null +++ b/packages/catalog-generator/src/main/resources/schemas/PipeErrorHandler.json @@ -0,0 +1,111 @@ +{ + "$schema" : "http://json-schema.org/draft-07/schema#", + "type" : "object", + "additionalProperties" : false, + "title": "Pipe Error Handler", + "description": "Camel K Pipe ErrorHandler. See https://camel.apache.org/camel-k/latest/pipe-step.html#_error_handler for more details.", + "oneOf" : [ { + "title": "No Pipe ErrorHandler", + "type": "object", + "properties" : { + "none" : { + "type" : "object" + } + }, + "required" : [ "none" ] + }, { + "title": "Log Pipe ErrorHandler", + "type": "object", + "properties" : { + "log": { + "type": "object", + "additionalProperties": false, + "properties": { + "parameters": { + "type": "object", + "properties": { + "maximumRedeliveries": { + "type": "number", + "description" : "Sets the maximum redeliveries x = redeliver at most x times 0 = no redeliveries -1 = redeliver forever" + }, + "redeliveryDelay": { + "type": "number", + "description" : "Sets the maximum delay between redelivery" + } + }, + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "required" : [ "log" ] + }, { + "title": "Sink Pipe ErrorHandler", + "type": "object", + "properties" : { + "sink": { + "type": "object", + "additionalProperties": false, + "properties": { + "endpoint": { + "type": "object", + "additionalProperties": false, + "properties": { + "ref": { + "type": "object", + "additionalProperties": false, + "properties": { + "kind": { + "type": "string" + }, + "apiVersion": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ "kind", "apiVersion", "name" ] + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "parameters": { + "type": "object", + "properties": { + "maximumRedeliveries": { + "type": "number", + "description" : "Sets the maximum redeliveries x = redeliver at most x times 0 = no redeliveries -1 = redeliver forever" + }, + "redeliveryDelay": { + "type": "number", + "description" : "Sets the maximum delay between redelivery" + } + }, + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "required" : [ "sink" ] + }], + "properties": { + "none": {}, + "log": {}, + "sink": {} + } +} diff --git a/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/MainTest.java b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/MainTest.java new file mode 100644 index 000000000..dddd1ab9b --- /dev/null +++ b/packages/catalog-generator/src/test/java/io/kaoto/camelcatalog/MainTest.java @@ -0,0 +1,13 @@ +package io.kaoto.camelcatalog; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +public class MainTest { + + @Test + public void testGenerate() throws IOException { + // Main.main(new String[] {}); + } +} diff --git a/packages/catalog-generator/tsconfig.json b/packages/catalog-generator/tsconfig.json new file mode 100644 index 000000000..5699396d8 --- /dev/null +++ b/packages/catalog-generator/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Node", + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + }, + "include": ["scripts"], +} diff --git a/yarn.lock b/yarn.lock index 7d6336503..ad1123838 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3672,6 +3672,23 @@ __metadata: languageName: unknown linkType: soft +"@kaoto/catalog-generator@workspace:packages/catalog-generator": + version: 0.0.0-use.local + resolution: "@kaoto/catalog-generator@workspace:packages/catalog-generator" + dependencies: + "@types/node": ^20.0.0 + eslint: ^8.45.0 + eslint-config-prettier: ^9.0.0 + eslint-plugin-import: ^2.26.0 + eslint-plugin-prettier: ^5.0.0 + json-schema-to-typescript: ^14.0.0 + prettier: ^3.0.0 + rimraf: ^5.0.1 + ts-node: ^10.9.1 + typescript: ^5.4.2 + languageName: unknown + linkType: soft + "@kaoto/kaoto-tests@workspace:packages/ui-tests": version: 0.0.0-use.local resolution: "@kaoto/kaoto-tests@workspace:packages/ui-tests" @@ -7087,9 +7104,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.17.0": - version: 4.17.4 - resolution: "@types/lodash@npm:4.17.4" - checksum: 268e652fd52d49189f155bc89b49bd4535aa44f0b6b0ed9ce7e50318307bda58147c49539d2047f39ca37cf5b5ea38dfb801d0dbcdbc8b019c95c1afc346b05a + version: 4.17.5 + resolution: "@types/lodash@npm:4.17.5" + checksum: 3c9bb15772509f0ecb40428531863dbc3f064f2bf34bbccc2ce2b2923c69fb0868aec7e357b1d97fd0d7f7e435a014ea5c1adef8a64715529887179c97a5a823 languageName: node linkType: hard @@ -17092,7 +17109,7 @@ __metadata: languageName: node linkType: hard -"prettier-fallback@npm:prettier@^3, prettier@npm:^3.1.1": +"prettier-fallback@npm:prettier@^3, prettier@npm:^3.1.1, prettier@npm:^3.2.5": version: 3.3.2 resolution: "prettier@npm:3.3.2" bin: @@ -17128,15 +17145,6 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.2.5": - version: 3.3.1 - resolution: "prettier@npm:3.3.1" - bin: - prettier: bin/prettier.cjs - checksum: 10987ff39e23d9359a76a441431dfe3ee26cc444540dc1577e8109e31394231fc1187d47a1e4ebc98bd605885c50ec681e9f2674e489c3313708c30b6ef5e119 - languageName: node - linkType: hard - "pretty-bytes@npm:^5.6.0": version: 5.6.0 resolution: "pretty-bytes@npm:5.6.0"